Compare commits
74
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a053a8d29 | ||
|
|
3c64f04f27 | ||
|
|
a5126309c2 | ||
|
|
ea04a292ed | ||
|
|
deeb6d4e3f | ||
|
|
a3bf753f4c | ||
|
|
459f225559 | ||
|
|
99de5f4257 | ||
|
|
4b417ce9e7 | ||
|
|
8697a30dc5 | ||
|
|
8565341682 | ||
|
|
9bd7daeaf8 | ||
|
|
abe22c8649 | ||
|
|
735a0bcc2c | ||
|
|
743b73d0af | ||
|
|
79a6599dc1 | ||
|
|
8d44829589 | ||
|
|
1d45c4f0e8 | ||
|
|
dc7f1597e5 | ||
|
|
7d954a1aa4 | ||
|
|
bc42aec68e | ||
|
|
3c8abd5b30 | ||
|
|
1602b071db | ||
|
|
4664402bf3 | ||
|
|
df7401f71c | ||
|
|
f9a1c98e38 | ||
|
|
80f3f31aee | ||
|
|
d4eff76f71 | ||
|
|
666d296160 | ||
|
|
a07f2a7c1b | ||
|
|
af650af489 | ||
|
|
2ee63656e2 | ||
|
|
27ec791353 | ||
|
|
ccb3672ff8 | ||
|
|
54faefb876 | ||
|
|
58c33743a1 | ||
|
|
b5f84a1625 | ||
|
|
c12abcbd42 | ||
|
|
6ac955eec0 | ||
|
|
501c5f0628 | ||
|
|
123765b676 | ||
|
|
bd571f5a7c | ||
|
|
9e8ea5d2f8 | ||
|
|
543dd1476d | ||
|
|
5f412b0baf | ||
|
|
98cb3e0bf8 | ||
|
|
16b09e8b3f | ||
|
|
653c47bd9d | ||
|
|
2553888a8b | ||
|
|
115ca2feb0 | ||
|
|
4cd9bcd938 | ||
|
|
244780eaa8 | ||
|
|
6609f7637a | ||
|
|
34e929c705 | ||
|
|
bcce9cf74b | ||
|
|
512334ee6b | ||
|
|
4b6b1fed71 | ||
|
|
1a07eb50c5 | ||
|
|
50b6c07114 | ||
|
|
939cf61b7c | ||
|
|
f12214f782 | ||
|
|
66fcaa5e01 | ||
|
|
a06964c0f1 | ||
|
|
333c1ca383 | ||
|
|
fb5cf32734 | ||
|
|
d024120afd | ||
|
|
ec6cb63f02 | ||
|
|
58acebc6e3 | ||
|
|
7a60a861f1 | ||
|
|
580d08a033 | ||
|
|
5e2678bc6d | ||
|
|
dba72e5a95 | ||
|
|
b522491310 | ||
|
|
d1ed7419ac |
@@ -73,15 +73,15 @@ jobs:
|
||||
libvips
|
||||
|
||||
- run:
|
||||
name: Install RVM and Ruby 3.3.3
|
||||
name: Install RVM and Ruby 3.4.4
|
||||
command: |
|
||||
sudo apt-get install -y gpg
|
||||
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
|
||||
\curl -sSL https://get.rvm.io | bash -s stable
|
||||
echo 'source ~/.rvm/scripts/rvm' >> $BASH_ENV
|
||||
source ~/.rvm/scripts/rvm
|
||||
rvm install "3.3.3"
|
||||
rvm use 3.3.3 --default
|
||||
rvm install "3.4.4"
|
||||
rvm use 3.4.4 --default
|
||||
gem install bundler -v 2.5.16
|
||||
|
||||
- run:
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
args:
|
||||
VARIANT: 'ubuntu-22.04'
|
||||
NODE_VERSION: '23.7.0'
|
||||
RUBY_VERSION: '3.3.3'
|
||||
RUBY_VERSION: '3.4.4'
|
||||
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||
USER_UID: '1000'
|
||||
USER_GID: '1000'
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
args:
|
||||
VARIANT: 'ubuntu-22.04'
|
||||
NODE_VERSION: '23.7.0'
|
||||
RUBY_VERSION: '3.3.3'
|
||||
RUBY_VERSION: '3.4.4'
|
||||
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
|
||||
USER_UID: '1000'
|
||||
USER_GID: '1000'
|
||||
|
||||
+179
-8
@@ -1,7 +1,10 @@
|
||||
require:
|
||||
plugins:
|
||||
- rubocop-performance
|
||||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
- rubocop-factory_bot
|
||||
|
||||
require:
|
||||
- ./rubocop/use_from_email.rb
|
||||
- ./rubocop/custom_cop_location.rb
|
||||
|
||||
@@ -13,44 +16,61 @@ Metrics/ClassLength:
|
||||
Exclude:
|
||||
- 'app/models/message.rb'
|
||||
- 'app/models/conversation.rb'
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 19
|
||||
Exclude:
|
||||
- 'enterprise/lib/captain/agent.rb'
|
||||
|
||||
RSpec/ExampleLength:
|
||||
Max: 25
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
Style/ExponentialNotation:
|
||||
Enabled: false
|
||||
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
|
||||
Style/SymbolArray:
|
||||
Enabled: false
|
||||
|
||||
Style/OpenStructUse:
|
||||
Enabled: false
|
||||
|
||||
Style/OptionalBooleanParameter:
|
||||
Exclude:
|
||||
- 'app/services/email_templates/db_resolver_service.rb'
|
||||
- 'app/dispatchers/dispatcher.rb'
|
||||
|
||||
Style/GlobalVars:
|
||||
Exclude:
|
||||
- 'config/initializers/01_redis.rb'
|
||||
- 'config/initializers/rack_attack.rb'
|
||||
- 'lib/redis/alfred.rb'
|
||||
- 'lib/global_config.rb'
|
||||
|
||||
Style/ClassVars:
|
||||
Exclude:
|
||||
- 'app/services/email_templates/db_resolver_service.rb'
|
||||
|
||||
Lint/MissingSuper:
|
||||
Exclude:
|
||||
- 'app/drops/base_drop.rb'
|
||||
|
||||
Lint/SymbolConversion:
|
||||
Enabled: false
|
||||
|
||||
Lint/EmptyBlock:
|
||||
Exclude:
|
||||
- 'app/views/api/v1/accounts/conversations/toggle_status.json.jbuilder'
|
||||
|
||||
Lint/OrAssignmentToConstant:
|
||||
Exclude:
|
||||
- 'lib/redis/config.rb'
|
||||
|
||||
Metrics/BlockLength:
|
||||
Max: 30
|
||||
Exclude:
|
||||
@@ -58,10 +78,16 @@ Metrics/BlockLength:
|
||||
- '**/routes.rb'
|
||||
- 'config/environments/*'
|
||||
- db/schema.rb
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Exclude:
|
||||
- lib/seeders/message_seeder.rb
|
||||
- spec/support/slack_stubs.rb
|
||||
|
||||
Rails/HelperInstanceVariable:
|
||||
Exclude:
|
||||
- enterprise/app/helpers/captain/chat_helper.rb
|
||||
|
||||
Rails/ApplicationController:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
||||
@@ -71,74 +97,101 @@ Rails/ApplicationController:
|
||||
- 'app/controllers/platform_controller.rb'
|
||||
- 'app/controllers/public_controller.rb'
|
||||
- 'app/controllers/survey/responses_controller.rb'
|
||||
|
||||
Rails/FindEach:
|
||||
Enabled: true
|
||||
Include:
|
||||
- 'app/**/*.rb'
|
||||
|
||||
Rails/CompactBlank:
|
||||
Enabled: false
|
||||
|
||||
Rails/EnvironmentVariableAccess:
|
||||
Enabled: false
|
||||
|
||||
Rails/TimeZoneAssignment:
|
||||
Enabled: false
|
||||
|
||||
Rails/RedundantPresenceValidationOnBelongsTo:
|
||||
Enabled: false
|
||||
|
||||
Rails/InverseOf:
|
||||
Exclude:
|
||||
- enterprise/app/models/captain/assistant.rb
|
||||
|
||||
Rails/UniqueValidationWithoutIndex:
|
||||
Exclude:
|
||||
- app/models/canned_response.rb
|
||||
- app/models/telegram_bot.rb
|
||||
- enterprise/app/models/captain_inbox.rb
|
||||
- 'app/models/channel/twitter_profile.rb'
|
||||
- 'app/models/webhook.rb'
|
||||
- 'app/models/contact.rb'
|
||||
|
||||
Style/ClassAndModuleChildren:
|
||||
EnforcedStyle: compact
|
||||
Exclude:
|
||||
- 'config/application.rb'
|
||||
- 'config/initializers/monkey_patches/*'
|
||||
|
||||
Style/MapToHash:
|
||||
Enabled: false
|
||||
|
||||
Style/HashSyntax:
|
||||
Enabled: true
|
||||
EnforcedStyle: no_mixed_keys
|
||||
EnforcedShorthandSyntax: never
|
||||
|
||||
RSpec/NestedGroups:
|
||||
Enabled: true
|
||||
Max: 4
|
||||
|
||||
RSpec/MessageSpies:
|
||||
Enabled: false
|
||||
|
||||
RSpec/StubbedMock:
|
||||
Enabled: false
|
||||
RSpec/FactoryBot/SyntaxMethods:
|
||||
Enabled: false
|
||||
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
|
||||
Naming/MemoizedInstanceVariableName:
|
||||
Exclude:
|
||||
- 'app/models/message.rb'
|
||||
|
||||
Style/GuardClause:
|
||||
Exclude:
|
||||
- 'app/builders/account_builder.rb'
|
||||
- 'app/models/attachment.rb'
|
||||
- 'app/models/message.rb'
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 26
|
||||
Exclude:
|
||||
- 'app/controllers/concerns/auth_helper.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'
|
||||
|
||||
Rails/ThreeStateBooleanColumn:
|
||||
Exclude:
|
||||
- 'db/migrate/20230503101201_create_sla_policies.rb'
|
||||
|
||||
RSpec/IndexedLet:
|
||||
Enabled: false
|
||||
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
|
||||
# we should bring this down
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 7
|
||||
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 14
|
||||
|
||||
@@ -166,3 +219,121 @@ AllCops:
|
||||
- 'tmp/**/*'
|
||||
- 'storage/**/*'
|
||||
- 'db/migrate/20230426130150_init_schema.rb'
|
||||
|
||||
FactoryBot/SyntaxMethods:
|
||||
Enabled: false
|
||||
|
||||
# Disable new rules causing errors
|
||||
Layout/LeadingCommentSpace:
|
||||
Enabled: false
|
||||
|
||||
Style/ReturnNilInPredicateMethodDefinition:
|
||||
Enabled: false
|
||||
|
||||
Style/RedundantParentheses:
|
||||
Enabled: false
|
||||
|
||||
Performance/StringIdentifierArgument:
|
||||
Enabled: false
|
||||
|
||||
Layout/EmptyLinesAroundExceptionHandlingKeywords:
|
||||
Enabled: false
|
||||
|
||||
Lint/LiteralAsCondition:
|
||||
Enabled: false
|
||||
|
||||
Style/RedundantReturn:
|
||||
Enabled: false
|
||||
|
||||
Layout/SpaceAroundOperators:
|
||||
Enabled: false
|
||||
|
||||
Rails/EnvLocal:
|
||||
Enabled: false
|
||||
|
||||
Rails/WhereRange:
|
||||
Enabled: false
|
||||
|
||||
Lint/UselessConstantScoping:
|
||||
Enabled: false
|
||||
|
||||
Style/MultipleComparison:
|
||||
Enabled: false
|
||||
|
||||
Bundler/OrderedGems:
|
||||
Enabled: false
|
||||
|
||||
RSpec/ExampleWording:
|
||||
Enabled: false
|
||||
|
||||
RSpec/ReceiveMessages:
|
||||
Enabled: false
|
||||
|
||||
FactoryBot/AssociationStyle:
|
||||
Enabled: false
|
||||
|
||||
Rails/EnumSyntax:
|
||||
Enabled: false
|
||||
|
||||
Lint/RedundantTypeConversion:
|
||||
Enabled: false
|
||||
|
||||
# Additional rules to disable
|
||||
Rails/RedundantActiveRecordAllMethod:
|
||||
Enabled: false
|
||||
|
||||
Layout/TrailingEmptyLines:
|
||||
Enabled: false
|
||||
|
||||
Style/SafeNavigationChainLength:
|
||||
Enabled: false
|
||||
|
||||
Lint/SafeNavigationConsistency:
|
||||
Enabled: false
|
||||
|
||||
Lint/CopDirectiveSyntax:
|
||||
Enabled: false
|
||||
|
||||
# Final set of rules to disable
|
||||
FactoryBot/ExcessiveCreateList:
|
||||
Enabled: false
|
||||
|
||||
RSpec/MissingExpectationTargetMethod:
|
||||
Enabled: false
|
||||
|
||||
Performance/InefficientHashSearch:
|
||||
Enabled: false
|
||||
|
||||
Style/RedundantSelfAssignmentBranch:
|
||||
Enabled: false
|
||||
|
||||
Style/YAMLFileRead:
|
||||
Enabled: false
|
||||
|
||||
Layout/ExtraSpacing:
|
||||
Enabled: false
|
||||
|
||||
Style/RedundantFilterChain:
|
||||
Enabled: false
|
||||
|
||||
Performance/MapMethodChain:
|
||||
Enabled: false
|
||||
|
||||
Rails/RootPathnameMethods:
|
||||
Enabled: false
|
||||
|
||||
Style/SuperArguments:
|
||||
Enabled: false
|
||||
|
||||
# Final remaining rules to disable
|
||||
Rails/Delegate:
|
||||
Enabled: false
|
||||
|
||||
Style/CaseLikeIf:
|
||||
Enabled: false
|
||||
|
||||
FactoryBot/RedundantFactoryOption:
|
||||
Enabled: false
|
||||
|
||||
FactoryBot/FactoryAssociationWithStrategy:
|
||||
Enabled: false
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.3.3
|
||||
3.4.4
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '3.3.3'
|
||||
ruby '3.4.4'
|
||||
|
||||
##-- base gems for rails --##
|
||||
gem 'rack-cors', '2.0.0', require: 'rack/cors'
|
||||
gem 'rails', '~> 7.0.8.4'
|
||||
gem 'rails', '~> 7.1'
|
||||
# Reduces boot times through caching; required in config/boot.rb
|
||||
gem 'bootsnap', require: false
|
||||
|
||||
@@ -33,6 +33,8 @@ gem 'liquid'
|
||||
gem 'commonmarker'
|
||||
# Validate Data against JSON Schema
|
||||
gem 'json_schemer'
|
||||
# used in swagger build
|
||||
gem 'json_refs'
|
||||
# Rack middleware for blocking & throttling abusive requests
|
||||
gem 'rack-attack', '>= 6.7.0'
|
||||
# a utility tool for streaming, flexible and safe downloading of remote files
|
||||
@@ -196,9 +198,6 @@ group :development do
|
||||
gem 'scss_lint', require: false
|
||||
gem 'web-console', '>= 4.2.1'
|
||||
|
||||
# used in swagger build
|
||||
gem 'json_refs'
|
||||
|
||||
# When we want to squash migrations
|
||||
gem 'squasher'
|
||||
|
||||
@@ -237,6 +236,7 @@ group :development, :test do
|
||||
gem 'rubocop-performance', require: false
|
||||
gem 'rubocop-rails', require: false
|
||||
gem 'rubocop-rspec', require: false
|
||||
gem 'rubocop-factory_bot', require: false
|
||||
gem 'seed_dump'
|
||||
gem 'shoulda-matchers'
|
||||
gem 'simplecov', '0.17.1', require: false
|
||||
|
||||
+160
-140
@@ -25,76 +25,89 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
actioncable (7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activejob (= 7.0.8.7)
|
||||
activerecord (= 7.0.8.7)
|
||||
activestorage (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
activejob (= 7.1.5.1)
|
||||
activerecord (= 7.1.5.1)
|
||||
activestorage (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
actionview (= 7.0.8.7)
|
||||
activejob (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
actionmailer (7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
actionview (= 7.1.5.1)
|
||||
activejob (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.8.7)
|
||||
actionview (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.1.5.1)
|
||||
actionview (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activerecord (= 7.0.8.7)
|
||||
activestorage (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
actiontext (7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
activerecord (= 7.1.5.1)
|
||||
activestorage (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
actionview (7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
activejob (7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
activerecord (7.0.8.7)
|
||||
activemodel (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
activerecord-import (1.4.1)
|
||||
activemodel (7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
activerecord (7.1.5.1)
|
||||
activemodel (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
timeout (>= 0.4.0)
|
||||
activerecord-import (2.1.0)
|
||||
activerecord (>= 4.2)
|
||||
activestorage (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activejob (= 7.0.8.7)
|
||||
activerecord (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
activestorage (7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
activejob (= 7.1.5.1)
|
||||
activerecord (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.8.7)
|
||||
activesupport (7.1.5.1)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
mutex_m
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0)
|
||||
acts-as-taggable-on (9.0.1)
|
||||
activerecord (>= 6.0, < 7.1)
|
||||
acts-as-taggable-on (12.0.0)
|
||||
activerecord (>= 7.1, < 8.1)
|
||||
zeitwerk (>= 2.4, < 3.0)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
administrate (0.20.1)
|
||||
@@ -116,7 +129,7 @@ GEM
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
ast (2.4.3)
|
||||
attr_extras (7.1.0)
|
||||
audited (5.4.1)
|
||||
activerecord (>= 5.0, < 7.7)
|
||||
@@ -142,14 +155,15 @@ GEM
|
||||
statsd-ruby (~> 1.1)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
bigdecimal (3.1.8)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.9)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.16.0)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (5.4.1)
|
||||
browser (5.3.1)
|
||||
builder (3.3.0)
|
||||
bullet (7.0.7)
|
||||
bullet (8.0.7)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundle-audit (0.1.0)
|
||||
@@ -161,8 +175,8 @@ GEM
|
||||
climate_control (1.2.0)
|
||||
coderay (1.1.3)
|
||||
commonmarker (0.23.10)
|
||||
concurrent-ruby (1.3.4)
|
||||
connection_pool (2.4.1)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.3)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
rexml
|
||||
@@ -176,16 +190,10 @@ GEM
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
datadog-ci (0.8.3)
|
||||
msgpack
|
||||
date (3.4.1)
|
||||
ddtrace (1.23.2)
|
||||
datadog-ci (~> 0.8.1)
|
||||
debase-ruby_core_source (= 3.3.1)
|
||||
libdatadog (~> 7.0.0.1.0)
|
||||
libddwaf (~> 1.14.0.0.0)
|
||||
ddtrace (0.48.0)
|
||||
ffi (~> 1.0)
|
||||
msgpack
|
||||
debase-ruby_core_source (3.3.1)
|
||||
debug (1.8.0)
|
||||
irb (>= 1.5.0)
|
||||
reline (>= 0.3.1)
|
||||
@@ -196,10 +204,10 @@ GEM
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise_token_auth (1.2.3)
|
||||
devise_token_auth (1.2.5)
|
||||
bcrypt (~> 3.0)
|
||||
devise (> 3.5.2, < 5)
|
||||
rails (>= 4.2.0, < 7.2)
|
||||
rails (>= 4.2.0, < 8.1)
|
||||
diff-lcs (1.5.1)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
@@ -212,6 +220,7 @@ GEM
|
||||
railties (>= 6.1)
|
||||
down (5.4.0)
|
||||
addressable (~> 2.8)
|
||||
drb (2.2.3)
|
||||
dry-cli (1.1.0)
|
||||
ecma-re-validator (0.4.0)
|
||||
regexp_parser (~> 2.2)
|
||||
@@ -254,7 +263,10 @@ GEM
|
||||
fcm (1.0.8)
|
||||
faraday (>= 1.0.0, < 3.0)
|
||||
googleauth (~> 1)
|
||||
ffi (1.16.3)
|
||||
ffi (1.17.2)
|
||||
ffi (1.17.2-arm64-darwin)
|
||||
ffi (1.17.2-x86_64-darwin)
|
||||
ffi (1.17.2-x86_64-linux-gnu)
|
||||
ffi-compiler (1.0.1)
|
||||
ffi (>= 1.0.0)
|
||||
rake
|
||||
@@ -315,16 +327,13 @@ GEM
|
||||
google-cloud-translate-v3 (0.10.0)
|
||||
gapic-common (>= 0.20.0, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-protobuf (3.25.5)
|
||||
google-protobuf (3.25.5-arm64-darwin)
|
||||
google-protobuf (3.25.5-x86_64-darwin)
|
||||
google-protobuf (3.25.5-x86_64-linux)
|
||||
google-protobuf (3.25.7)
|
||||
googleapis-common-protos (1.6.0)
|
||||
google-protobuf (>= 3.18, < 5.a)
|
||||
googleapis-common-protos-types (~> 1.7)
|
||||
grpc (~> 1.41)
|
||||
googleapis-common-protos-types (1.14.0)
|
||||
google-protobuf (~> 3.18)
|
||||
googleapis-common-protos-types (1.20.0)
|
||||
google-protobuf (>= 3.18, < 5.a)
|
||||
googleauth (1.11.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.1)
|
||||
@@ -334,17 +343,17 @@ GEM
|
||||
signet (>= 0.16, < 2.a)
|
||||
groupdate (6.2.1)
|
||||
activesupport (>= 5.2)
|
||||
grpc (1.62.0)
|
||||
google-protobuf (~> 3.25)
|
||||
grpc (1.72.0)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.62.0-arm64-darwin)
|
||||
google-protobuf (~> 3.25)
|
||||
grpc (1.72.0-arm64-darwin)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.62.0-x86_64-darwin)
|
||||
google-protobuf (~> 3.25)
|
||||
grpc (1.72.0-x86_64-darwin)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.62.0-x86_64-linux)
|
||||
google-protobuf (~> 3.25)
|
||||
grpc (1.72.0-x86_64-linux)
|
||||
google-protobuf (>= 3.25, < 5.0)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
hairtrigger (1.0.0)
|
||||
@@ -370,7 +379,7 @@ GEM
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.6)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
@@ -388,7 +397,7 @@ GEM
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.6.3)
|
||||
json (2.12.0)
|
||||
json_refs (0.1.8)
|
||||
hana
|
||||
json_schemer (0.2.24)
|
||||
@@ -423,21 +432,13 @@ GEM
|
||||
faraday-multipart
|
||||
json (>= 1.8)
|
||||
rexml
|
||||
language_server-protocol (3.17.0.5)
|
||||
launchy (2.5.2)
|
||||
addressable (~> 2.8)
|
||||
letter_opener (1.8.1)
|
||||
launchy (>= 2.2, < 3)
|
||||
libdatadog (7.0.0.1.0)
|
||||
libdatadog (7.0.0.1.0-x86_64-linux)
|
||||
libddwaf (1.14.0.0.0)
|
||||
ffi (~> 1.0)
|
||||
libddwaf (1.14.0.0.0-arm64-darwin)
|
||||
ffi (~> 1.0)
|
||||
libddwaf (1.14.0.0.0-x86_64-darwin)
|
||||
ffi (~> 1.0)
|
||||
libddwaf (1.14.0.0.0-x86_64-linux)
|
||||
ffi (~> 1.0)
|
||||
line-bot-api (1.28.0)
|
||||
lint_roller (1.1.0)
|
||||
liquid (5.4.0)
|
||||
listen (3.8.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
@@ -445,7 +446,7 @@ GEM
|
||||
llhttp-ffi (0.4.0)
|
||||
ffi-compiler (~> 1.0)
|
||||
rake (~> 13.0)
|
||||
logger (1.6.0)
|
||||
logger (1.7.0)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
@@ -471,10 +472,10 @@ GEM
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.8)
|
||||
minitest (5.25.4)
|
||||
minitest (5.25.5)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
msgpack (1.7.0)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.3.0)
|
||||
@@ -545,14 +546,16 @@ GEM
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.1)
|
||||
parallel (1.23.0)
|
||||
parser (3.2.2.1)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.8.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.3)
|
||||
pg_search (2.3.6)
|
||||
activerecord (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
pgvector (0.1.1)
|
||||
prism (1.4.0)
|
||||
procore-sift (1.0.0)
|
||||
activerecord (>= 6.1)
|
||||
pry (0.14.2)
|
||||
@@ -567,7 +570,7 @@ GEM
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.14)
|
||||
rack (2.2.15)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-contrib (2.5.0)
|
||||
@@ -581,23 +584,28 @@ GEM
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (1.0.2)
|
||||
rack (< 3)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rack-timeout (0.6.3)
|
||||
rails (7.0.8.7)
|
||||
actioncable (= 7.0.8.7)
|
||||
actionmailbox (= 7.0.8.7)
|
||||
actionmailer (= 7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
actiontext (= 7.0.8.7)
|
||||
actionview (= 7.0.8.7)
|
||||
activejob (= 7.0.8.7)
|
||||
activemodel (= 7.0.8.7)
|
||||
activerecord (= 7.0.8.7)
|
||||
activestorage (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
rackup (1.0.1)
|
||||
rack (< 3)
|
||||
webrick
|
||||
rails (7.1.5.1)
|
||||
actioncable (= 7.1.5.1)
|
||||
actionmailbox (= 7.1.5.1)
|
||||
actionmailer (= 7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
actiontext (= 7.1.5.1)
|
||||
actionview (= 7.1.5.1)
|
||||
activejob (= 7.1.5.1)
|
||||
activemodel (= 7.1.5.1)
|
||||
activerecord (= 7.1.5.1)
|
||||
activestorage (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.8.7)
|
||||
railties (= 7.1.5.1)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -605,13 +613,14 @@ GEM
|
||||
rails-html-sanitizer (1.6.1)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
railties (7.0.8.7)
|
||||
actionpack (= 7.0.8.7)
|
||||
activesupport (= 7.0.8.7)
|
||||
method_source
|
||||
railties (7.1.5.1)
|
||||
actionpack (= 7.1.5.1)
|
||||
activesupport (= 7.1.5.1)
|
||||
irb
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rb-fsevent (0.11.2)
|
||||
@@ -623,7 +632,7 @@ GEM
|
||||
connection_pool
|
||||
redis-namespace (1.10.0)
|
||||
redis (>= 4)
|
||||
regexp_parser (2.8.0)
|
||||
regexp_parser (2.10.0)
|
||||
reline (0.3.6)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
@@ -643,7 +652,7 @@ GEM
|
||||
retriable (3.1.2)
|
||||
reverse_markdown (2.1.1)
|
||||
nokogiri
|
||||
rexml (3.3.9)
|
||||
rexml (3.4.1)
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.2)
|
||||
@@ -663,30 +672,36 @@ GEM
|
||||
rspec-support (3.13.1)
|
||||
rspec_junit_formatter (0.6.0)
|
||||
rspec-core (>= 2, < 4, != 2.12.0)
|
||||
rubocop (1.50.2)
|
||||
rubocop (1.75.6)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.2.0.0)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.28.0, < 2.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.28.1)
|
||||
parser (>= 3.2.1.0)
|
||||
rubocop-capybara (2.18.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-performance (1.17.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.19.1)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.44.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-factory_bot (2.27.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-performance (1.25.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-rails (2.32.0)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-rspec (2.21.0)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rspec (3.6.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
ruby-openai (7.3.1)
|
||||
event_stream_parser (>= 0.3.0, < 2.0.0)
|
||||
faraday (>= 1)
|
||||
@@ -816,8 +831,10 @@ GEM
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (2.4.2)
|
||||
uniform_notifier (1.16.0)
|
||||
unicode-display_width (3.1.4)
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
uniform_notifier (1.17.0)
|
||||
uri (1.0.3)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (5.2.6)
|
||||
@@ -845,7 +862,9 @@ GEM
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
websocket-driver (0.7.6)
|
||||
webrick (1.9.1)
|
||||
websocket-driver (0.7.7)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
wisper (2.0.0)
|
||||
@@ -951,7 +970,7 @@ DEPENDENCIES
|
||||
rack-cors (= 2.0.0)
|
||||
rack-mini-profiler (>= 3.2.0)
|
||||
rack-timeout
|
||||
rails (~> 7.0.8.4)
|
||||
rails (~> 7.1)
|
||||
redis
|
||||
redis-namespace
|
||||
responders (>= 3.1.1)
|
||||
@@ -960,6 +979,7 @@ DEPENDENCIES
|
||||
rspec-rails (>= 6.1.5)
|
||||
rspec_junit_formatter
|
||||
rubocop
|
||||
rubocop-factory_bot
|
||||
rubocop-performance
|
||||
rubocop-rails
|
||||
rubocop-rspec
|
||||
@@ -997,7 +1017,7 @@ DEPENDENCIES
|
||||
working_hours
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.3.3p89
|
||||
ruby 3.4.4p34
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.16
|
||||
|
||||
@@ -32,14 +32,7 @@ class AccountBuilder
|
||||
end
|
||||
|
||||
def validate_email
|
||||
raise InvalidEmail.new({ domain_blocked: domain_blocked }) if domain_blocked?
|
||||
|
||||
address = ValidEmail2::Address.new(@email)
|
||||
if address.valid? && !address.disposable?
|
||||
true
|
||||
else
|
||||
raise InvalidEmail.new({ valid: address.valid?, disposable: address.disposable? })
|
||||
end
|
||||
Account::SignUpEmailValidationService.new(@email).perform
|
||||
end
|
||||
|
||||
def validate_user
|
||||
@@ -81,21 +74,4 @@ class AccountBuilder
|
||||
@user.confirm if @confirmed
|
||||
@user.save!
|
||||
end
|
||||
|
||||
def domain_blocked?
|
||||
domain = @email.split('@').last
|
||||
|
||||
blocked_domains.each do |blocked_domain|
|
||||
return true if domain.match?(blocked_domain)
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def blocked_domains
|
||||
domains = GlobalConfigService.load('BLOCKED_EMAIL_DOMAINS', '')
|
||||
return [] if domains.blank?
|
||||
|
||||
domains.split("\n").map(&:strip)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -59,11 +59,13 @@ class ContactInboxBuilder
|
||||
end
|
||||
|
||||
def create_contact_inbox
|
||||
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
|
||||
attrs = {
|
||||
contact_id: @contact.id,
|
||||
inbox_id: @inbox.id,
|
||||
source_id: @source_id
|
||||
)
|
||||
}
|
||||
|
||||
::ContactInbox.where(attrs).first_or_create!(hmac_verified: hmac_verified || false)
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
Rails.logger.info("[ContactInboxBuilder] RecordNotUnique #{@source_id} #{@contact.id} #{@inbox.id}")
|
||||
update_old_contact_inbox
|
||||
|
||||
@@ -152,11 +152,13 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
|
||||
end
|
||||
|
||||
def message_already_exists?
|
||||
cw_message = conversation.messages.where(
|
||||
source_id: @messaging[:message][:mid]
|
||||
).first
|
||||
find_message_by_source_id(@messaging[:message][:mid]).present?
|
||||
end
|
||||
|
||||
cw_message.present?
|
||||
def find_message_by_source_id(source_id)
|
||||
return unless source_id
|
||||
|
||||
@message = Message.find_by(source_id: source_id)
|
||||
end
|
||||
|
||||
def all_unsupported_files?
|
||||
|
||||
@@ -68,7 +68,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||
|
||||
def article_params
|
||||
params.require(:article).permit(
|
||||
:title, :slug, :position, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status,
|
||||
:title, :slug, :position, :content, :description, :category_id, :author_id, :associated_article_id, :status,
|
||||
:locale, meta: [:title,
|
||||
:description,
|
||||
{ tags: [] }]
|
||||
|
||||
@@ -12,10 +12,6 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
|
||||
Current.account
|
||||
).perform
|
||||
|
||||
# Only allow conversations from inboxes the user has access to
|
||||
inbox_ids = Current.user.assigned_inboxes.pluck(:id)
|
||||
conversations = conversations.where(inbox_id: inbox_ids)
|
||||
|
||||
@conversations = conversations.order(last_activity_at: :desc).limit(20)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -94,7 +94,8 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:team_id, :project_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, label_ids: [])
|
||||
params.permit(:team_id, :project_id, :conversation_id, :issue_id, :link_id, :title, :description, :assignee_id, :priority, :state_id,
|
||||
label_ids: [])
|
||||
end
|
||||
|
||||
def fetch_hook
|
||||
|
||||
@@ -21,7 +21,7 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
|
||||
|
||||
def sign_up_user
|
||||
return redirect_to login_page_url(error: 'no-account-found') unless account_signup_allowed?
|
||||
return redirect_to login_page_url(error: 'business-account-only') unless validate_business_account?
|
||||
return redirect_to login_page_url(error: 'business-account-only') unless validate_signup_email_is_business_domain?
|
||||
|
||||
create_account_for_user
|
||||
token = @resource.send(:set_reset_password_token)
|
||||
@@ -53,9 +53,11 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
|
||||
).first
|
||||
end
|
||||
|
||||
def validate_business_account?
|
||||
# return true if the user is a business account, false if it is a gmail account
|
||||
auth_hash['info']['email'].downcase.exclude?('@gmail.com')
|
||||
def validate_signup_email_is_business_domain?
|
||||
# return true if the user is a business account, false if it is a blocked domain account
|
||||
Account::SignUpEmailValidationService.new(auth_hash['info']['email']).perform
|
||||
rescue CustomExceptions::Account::InvalidEmail
|
||||
false
|
||||
end
|
||||
|
||||
def create_account_for_user
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
class Platform::Api::V1::UsersController < PlatformController
|
||||
# ref: https://stackoverflow.com/a/45190318/939299
|
||||
# set resource is called for other actions already in platform controller
|
||||
# we want to add login to that chain as well
|
||||
before_action(only: [:login]) { set_resource }
|
||||
before_action(only: [:login]) { validate_platform_app_permissible }
|
||||
# we want to add login and token to that chain as well
|
||||
before_action(only: [:login, :token]) { set_resource }
|
||||
before_action(only: [:login, :token]) { validate_platform_app_permissible }
|
||||
|
||||
def show; end
|
||||
|
||||
@@ -18,6 +18,8 @@ class Platform::Api::V1::UsersController < PlatformController
|
||||
render json: { url: @resource.generate_sso_link }
|
||||
end
|
||||
|
||||
def token; end
|
||||
|
||||
def update
|
||||
@resource.assign_attributes(user_update_params)
|
||||
|
||||
|
||||
@@ -88,7 +88,10 @@ class ConversationFinder
|
||||
|
||||
def find_conversation_by_inbox
|
||||
@conversations = current_account.conversations
|
||||
@conversations = @conversations.where(inbox_id: @inbox_ids) unless params[:inbox_id].blank? && @is_admin
|
||||
|
||||
return unless params[:inbox_id]
|
||||
|
||||
@conversations = @conversations.where(inbox_id: @inbox_ids)
|
||||
end
|
||||
|
||||
def find_all_conversations
|
||||
|
||||
@@ -134,10 +134,6 @@ class ConversationApi extends ApiClient {
|
||||
return axios.get(`${this.url}/${conversationId}/attachments`);
|
||||
}
|
||||
|
||||
requestCopilot(conversationId, body) {
|
||||
return axios.post(`${this.url}/${conversationId}/copilot`, body);
|
||||
}
|
||||
|
||||
getInboxAssistant(conversationId) {
|
||||
return axios.get(`${this.url}/${conversationId}/inbox_assistant`);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.tabs--container--with-border {
|
||||
@apply border-b border-n-weak;
|
||||
@apply border-b border-b-n-weak;
|
||||
}
|
||||
|
||||
.tabs--container--compact.tab--chat-type {
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ConfirmContactDeleteDialog from 'dashboard/components-next/Contacts/ContactsForm/ConfirmContactDeleteDialog.vue';
|
||||
|
||||
defineProps({
|
||||
selectedContact: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const [showDeleteSection, toggleDeleteSection] = useToggle();
|
||||
const confirmDeleteContactDialogRef = ref(null);
|
||||
|
||||
const openConfirmDeleteContactDialog = () => {
|
||||
confirmDeleteContactDialogRef.value?.dialogRef.open();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-start border-t border-n-strong px-6 py-5">
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT')"
|
||||
sm
|
||||
link
|
||||
slate
|
||||
class="hover:!no-underline text-n-slate-12"
|
||||
icon="i-lucide-chevron-down"
|
||||
trailing-icon
|
||||
@click="toggleDeleteSection()"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="transition-all duration-300 ease-in-out grid w-full overflow-hidden"
|
||||
:class="
|
||||
showDeleteSection
|
||||
? 'grid-rows-[1fr] opacity-100 mt-2'
|
||||
: 'grid-rows-[0fr] opacity-0 mt-0'
|
||||
"
|
||||
>
|
||||
<div class="overflow-hidden min-h-0">
|
||||
<span class="inline-flex text-n-slate-11 text-sm items-center gap-1">
|
||||
{{ t('CONTACTS_LAYOUT.CARD.DELETE_CONTACT.MESSAGE') }}
|
||||
<Button
|
||||
:label="t('CONTACTS_LAYOUT.CARD.DELETE_CONTACT.BUTTON')"
|
||||
sm
|
||||
ruby
|
||||
link
|
||||
@click="openConfirmDeleteContactDialog()"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmContactDeleteDialog
|
||||
ref="confirmDeleteContactDialogRef"
|
||||
:selected-contact="selectedContact"
|
||||
/>
|
||||
</template>
|
||||
@@ -7,6 +7,7 @@ import ContactsForm from 'dashboard/components-next/Contacts/ContactsForm/Contac
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import Flag from 'dashboard/components-next/flag/Flag.vue';
|
||||
import ContactDeleteSection from 'dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue';
|
||||
import countries from 'shared/constants/countries';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -149,15 +150,15 @@ const onClickViewDetails = () => emit('showContact', props.id);
|
||||
/>
|
||||
|
||||
<template #after>
|
||||
<transition
|
||||
enter-active-class="overflow-hidden transition-all duration-300 ease-out"
|
||||
leave-active-class="overflow-hidden transition-all duration-300 ease-in"
|
||||
enter-from-class="overflow-hidden opacity-0 max-h-0"
|
||||
enter-to-class="opacity-100 max-h-[690px] sm:max-h-[470px] md:max-h-[410px]"
|
||||
leave-from-class="opacity-100 max-h-[690px] sm:max-h-[470px] md:max-h-[410px]"
|
||||
leave-to-class="overflow-hidden opacity-0 max-h-0"
|
||||
<div
|
||||
class="transition-all duration-500 ease-in-out grid overflow-hidden"
|
||||
:class="
|
||||
isExpanded
|
||||
? 'grid-rows-[1fr] opacity-100'
|
||||
: 'grid-rows-[0fr] opacity-0'
|
||||
"
|
||||
>
|
||||
<div v-show="isExpanded" class="w-full">
|
||||
<div class="overflow-hidden">
|
||||
<div class="flex flex-col gap-6 p-6 border-t border-n-strong">
|
||||
<ContactsForm
|
||||
ref="contactsFormRef"
|
||||
@@ -176,8 +177,14 @@ const onClickViewDetails = () => emit('showContact', props.id);
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ContactDeleteSection
|
||||
:selected-contact="{
|
||||
id: props.id,
|
||||
name: props.name,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
</CardLayout>
|
||||
</template>
|
||||
|
||||
+1
-5
@@ -47,11 +47,7 @@ defineExpose({ dialogRef });
|
||||
ref="dialogRef"
|
||||
type="alert"
|
||||
:title="t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.TITLE')"
|
||||
:description="
|
||||
t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.DESCRIPTION', {
|
||||
contactName: props.selectedContact.name,
|
||||
})
|
||||
"
|
||||
:description="t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.DESCRIPTION')"
|
||||
:confirm-button-label="t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.CONFIRM')"
|
||||
@confirm="handleDialogConfirm"
|
||||
/>
|
||||
|
||||
@@ -117,7 +117,7 @@ const STYLE_CONFIG = {
|
||||
'text-n-ruby-11 hover:enabled:bg-n-ruby-9/10 focus-visible:bg-n-ruby-9/10 outline-n-ruby-8',
|
||||
ghost:
|
||||
'text-n-ruby-11 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
|
||||
link: 'text-n-ruby-9 hover:enabled:underline focus-visible:underline outline-transparent',
|
||||
link: 'text-n-ruby-9 dark:text-n-ruby-11 hover:enabled:underline focus-visible:underline outline-transparent',
|
||||
},
|
||||
amber: {
|
||||
solid:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { nextTick, ref, watch, computed } from 'vue';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
@@ -8,8 +7,10 @@ import CopilotInput from './CopilotInput.vue';
|
||||
import CopilotLoader from './CopilotLoader.vue';
|
||||
import CopilotAgentMessage from './CopilotAgentMessage.vue';
|
||||
import CopilotAssistantMessage from './CopilotAssistantMessage.vue';
|
||||
import CopilotThinkingGroup from './CopilotThinkingGroup.vue';
|
||||
import ToggleCopilotAssistant from './ToggleCopilotAssistant.vue';
|
||||
import Icon from '../icon/Icon.vue';
|
||||
import CopilotHeader from './CopilotHeader.vue';
|
||||
import CopilotEmptyState from './CopilotEmptyState.vue';
|
||||
|
||||
const props = defineProps({
|
||||
supportAgent: {
|
||||
@@ -26,7 +27,7 @@ const props = defineProps({
|
||||
},
|
||||
conversationInboxType: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
assistants: {
|
||||
type: Array,
|
||||
@@ -38,22 +39,13 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['sendMessage', 'reset', 'setAssistant']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const COPILOT_USER_ROLES = ['assistant', 'system'];
|
||||
const emit = defineEmits(['sendMessage', 'reset', 'setAssistant', 'close']);
|
||||
|
||||
const sendMessage = message => {
|
||||
emit('sendMessage', message);
|
||||
useTrack(COPILOT_EVENTS.SEND_MESSAGE);
|
||||
};
|
||||
|
||||
const useSuggestion = opt => {
|
||||
emit('sendMessage', t(opt.prompt));
|
||||
useTrack(COPILOT_EVENTS.SEND_SUGGESTED);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset');
|
||||
};
|
||||
@@ -67,20 +59,28 @@ const scrollToBottom = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const promptOptions = [
|
||||
{
|
||||
label: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.LABEL',
|
||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.CONTENT',
|
||||
},
|
||||
{
|
||||
label: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.LABEL',
|
||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.CONTENT',
|
||||
},
|
||||
{
|
||||
label: 'CAPTAIN.COPILOT.PROMPTS.RATE.LABEL',
|
||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.RATE.CONTENT',
|
||||
},
|
||||
];
|
||||
const groupedMessages = computed(() => {
|
||||
const result = [];
|
||||
let thinkingGroup = [];
|
||||
|
||||
props.messages.forEach(message => {
|
||||
if (message.role === 'assistant_thinking') {
|
||||
thinkingGroup.push(message);
|
||||
} else {
|
||||
if (thinkingGroup.length > 0) {
|
||||
result.push({ type: 'thinking_group', messages: thinkingGroup });
|
||||
thinkingGroup = [];
|
||||
}
|
||||
result.push({ type: 'message', message });
|
||||
}
|
||||
});
|
||||
|
||||
if (thinkingGroup.length > 0) {
|
||||
result.push({ type: 'thinking_group', messages: thinkingGroup });
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => props.messages, () => props.isCaptainTyping],
|
||||
@@ -93,60 +93,62 @@ watch(
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full text-sm leading-6 tracking-tight w-full">
|
||||
<div ref="chatContainer" class="flex-1 px-4 py-4 space-y-6 overflow-y-auto">
|
||||
<template v-for="message in messages" :key="message.id">
|
||||
<CopilotAgentMessage
|
||||
v-if="message.role === 'user'"
|
||||
:support-agent="supportAgent"
|
||||
:message="message"
|
||||
/>
|
||||
<CopilotAssistantMessage
|
||||
v-else-if="COPILOT_USER_ROLES.includes(message.role)"
|
||||
:message="message"
|
||||
:conversation-inbox-type="conversationInboxType"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<CopilotLoader v-if="isCaptainTyping" />
|
||||
</div>
|
||||
|
||||
<CopilotHeader
|
||||
:has-messages="messages.length > 0"
|
||||
@reset="handleReset"
|
||||
@close="$emit('close')"
|
||||
/>
|
||||
<div
|
||||
v-if="!messages.length"
|
||||
class="h-full w-full flex items-center justify-center"
|
||||
ref="chatContainer"
|
||||
class="flex-1 flex px-4 py-4 overflow-y-auto items-start"
|
||||
>
|
||||
<div class="h-fit px-3 py-3 space-y-1">
|
||||
<span class="text-xs text-n-slate-10">
|
||||
{{ $t('COPILOT.TRY_THESE_PROMPTS') }}
|
||||
</span>
|
||||
<button
|
||||
v-for="prompt in promptOptions"
|
||||
:key="prompt.label"
|
||||
class="px-2 py-1 rounded-md border border-n-weak bg-n-slate-2 text-n-slate-11 flex items-center gap-1"
|
||||
@click="() => useSuggestion(prompt)"
|
||||
<div v-if="messages.length" class="space-y-6 flex-1 flex flex-col w-full">
|
||||
<template
|
||||
v-for="item in groupedMessages"
|
||||
:key="item.type === 'message' ? item.message.id : 'thinking-group'"
|
||||
>
|
||||
<span>{{ t(prompt.label) }}</span>
|
||||
<Icon icon="i-lucide-chevron-right" />
|
||||
</button>
|
||||
<template v-if="item.type === 'message'">
|
||||
<CopilotAgentMessage
|
||||
v-if="item.message.role === 'user'"
|
||||
:support-agent="supportAgent"
|
||||
:message="item.message"
|
||||
/>
|
||||
<CopilotAssistantMessage
|
||||
v-else-if="
|
||||
item.message.role === 'assistant' ||
|
||||
item.message.role === 'system'
|
||||
"
|
||||
:message="item.message"
|
||||
:conversation-inbox-type="conversationInboxType"
|
||||
/>
|
||||
</template>
|
||||
<CopilotThinkingGroup
|
||||
v-else
|
||||
:messages="item.messages"
|
||||
:has-assistant-message-after="
|
||||
groupedMessages[groupedMessages.indexOf(item) + 1]?.message
|
||||
?.role === 'assistant'
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<CopilotLoader v-if="isCaptainTyping" />
|
||||
</div>
|
||||
<CopilotEmptyState
|
||||
v-if="!messages.length"
|
||||
@use-suggestion="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mx-3 mt-px mb-2">
|
||||
<div class="flex items-center gap-2 justify-between w-full mb-1">
|
||||
<ToggleCopilotAssistant
|
||||
v-if="assistants.length"
|
||||
v-if="assistants.length > 1"
|
||||
:assistants="assistants"
|
||||
:active-assistant="activeAssistant"
|
||||
@set-assistant="$event => emit('setAssistant', $event)"
|
||||
/>
|
||||
<div v-else />
|
||||
<button
|
||||
v-if="messages.length"
|
||||
class="text-xs flex items-center gap-1 hover:underline"
|
||||
@click="handleReset"
|
||||
>
|
||||
<i class="i-lucide-refresh-ccw" />
|
||||
<span>{{ $t('CAPTAIN.COPILOT.RESET') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<CopilotInput class="mb-1 w-full" @send="sendMessage" />
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,17 @@
|
||||
<script setup>
|
||||
import Avatar from '../avatar/Avatar.vue';
|
||||
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
supportAgent: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Avatar
|
||||
:name="supportAgent.available_name"
|
||||
:src="supportAgent.avatar_url"
|
||||
:size="24"
|
||||
rounded-full
|
||||
/>
|
||||
<div class="space-y-1 text-n-slate-12">
|
||||
<div class="font-medium">{{ $t('CAPTAIN.COPILOT.YOU') }}</div>
|
||||
<div class="break-words">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
<div class="space-y-1 text-n-slate-12">
|
||||
<div class="font-medium">{{ $t('CAPTAIN.COPILOT.YOU') }}</div>
|
||||
<div class="break-words">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import MessageFormatter from 'shared/helpers/MessageFormatter.js';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Avatar from '../avatar/Avatar.vue';
|
||||
|
||||
const props = defineProps({
|
||||
message: {
|
||||
@@ -18,7 +17,7 @@ const props = defineProps({
|
||||
},
|
||||
conversationInboxType: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -46,33 +45,25 @@ const useCopilotResponse = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row gap-2">
|
||||
<Avatar
|
||||
name="Captain Copilot"
|
||||
icon-name="i-woot-captain"
|
||||
:size="24"
|
||||
rounded-full
|
||||
<div class="flex flex-col gap-1 text-n-slate-12">
|
||||
<div class="font-medium">{{ $t('CAPTAIN.NAME') }}</div>
|
||||
<span v-if="hasEmptyMessageContent" class="text-n-ruby-11">
|
||||
{{ $t('CAPTAIN.COPILOT.EMPTY_MESSAGE') }}
|
||||
</span>
|
||||
<div
|
||||
v-else
|
||||
v-dompurify-html="messageContent"
|
||||
class="prose-sm break-words"
|
||||
/>
|
||||
<div class="flex flex-col gap-1 text-n-slate-12">
|
||||
<div class="font-medium">{{ $t('CAPTAIN.NAME') }}</div>
|
||||
<span v-if="hasEmptyMessageContent" class="text-n-ruby-11">
|
||||
{{ $t('CAPTAIN.COPILOT.EMPTY_MESSAGE') }}
|
||||
</span>
|
||||
<div
|
||||
v-else
|
||||
v-dompurify-html="messageContent"
|
||||
class="prose-sm break-words"
|
||||
<div class="flex flex-row mt-1">
|
||||
<Button
|
||||
v-if="!hasEmptyMessageContent"
|
||||
:label="$t('CAPTAIN.COPILOT.USE')"
|
||||
faded
|
||||
sm
|
||||
slate
|
||||
@click="useCopilotResponse"
|
||||
/>
|
||||
<div class="flex flex-row mt-1">
|
||||
<Button
|
||||
v-if="!hasEmptyMessageContent"
|
||||
:label="$t('CAPTAIN.COPILOT.USE')"
|
||||
faded
|
||||
sm
|
||||
slate
|
||||
@click="useCopilotResponse"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import Icon from '../icon/Icon.vue';
|
||||
|
||||
const emit = defineEmits(['useSuggestion']);
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
const routePromptMap = {
|
||||
conversations: [
|
||||
{
|
||||
label: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.LABEL',
|
||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUMMARIZE.CONTENT',
|
||||
},
|
||||
{
|
||||
label: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.LABEL',
|
||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.SUGGEST.CONTENT',
|
||||
},
|
||||
{
|
||||
label: 'CAPTAIN.COPILOT.PROMPTS.RATE.LABEL',
|
||||
prompt: 'CAPTAIN.COPILOT.PROMPTS.RATE.CONTENT',
|
||||
},
|
||||
],
|
||||
dashboard: [
|
||||
{
|
||||
label: 'High priority conversations',
|
||||
prompt: 'Get me a summary of all high priority open conversations',
|
||||
},
|
||||
{
|
||||
label: 'List contacts',
|
||||
prompt: 'Show me the list of contacts',
|
||||
},
|
||||
{
|
||||
label: 'Summarize articles',
|
||||
prompt: 'List articles that are in draft',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const getCurrentRoute = () => {
|
||||
const path = route.path;
|
||||
if (path.includes('/conversations')) return 'conversations';
|
||||
if (path.includes('/dashboard')) return 'dashboard';
|
||||
if (path.includes('/contacts')) return 'contacts';
|
||||
if (path.includes('/articles')) return 'articles';
|
||||
return 'dashboard';
|
||||
};
|
||||
|
||||
const promptOptions = computed(() => {
|
||||
const currentRoute = getCurrentRoute();
|
||||
return routePromptMap[currentRoute] || routePromptMap.conversations;
|
||||
});
|
||||
|
||||
const handleSuggestion = opt => {
|
||||
emit('useSuggestion', t(opt.prompt));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 flex flex-col gap-6 px-2">
|
||||
<div class="flex flex-col space-y-4 py-4">
|
||||
<Icon icon="i-woot-captain" class="text-n-slate-9 text-4xl" />
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-base font-medium text-n-slate-12 leading-8">
|
||||
{{ $t('CAPTAIN.COPILOT.PANEL_TITLE') }}
|
||||
</h3>
|
||||
<p class="text-sm text-n-slate-11 leading-6">
|
||||
{{ $t('CAPTAIN.COPILOT.KICK_OFF_MESSAGE') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full space-y-2">
|
||||
<span class="text-xs text-n-slate-10 block">
|
||||
{{ $t('CAPTAIN.COPILOT.TRY_THESE_PROMPTS') }}
|
||||
</span>
|
||||
<div class="space-y-1">
|
||||
<button
|
||||
v-for="prompt in promptOptions"
|
||||
:key="prompt.label"
|
||||
class="w-full px-3 py-2 rounded-md border border-n-weak bg-n-slate-2 text-n-slate-11 flex items-center justify-between hover:bg-n-slate-3 transition-colors"
|
||||
@click="handleSuggestion(prompt)"
|
||||
>
|
||||
<span>{{ t(prompt.label) }}</span>
|
||||
<Icon icon="i-lucide-chevron-right" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import CopilotHeader from './CopilotHeader.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story
|
||||
title="Captain/Copilot/CopilotHeader"
|
||||
:layout="{ type: 'grid', width: '800px' }"
|
||||
>
|
||||
<!-- Default State -->
|
||||
<Variant title="Default State">
|
||||
<CopilotHeader />
|
||||
</Variant>
|
||||
|
||||
<!-- With New Conversation Button -->
|
||||
<Variant title="With New Conversation Button">
|
||||
<!-- eslint-disable-next-line vue/prefer-true-attribute-shorthand -->
|
||||
<CopilotHeader :has-messages="true" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
import Button from '../button/Button.vue';
|
||||
defineProps({
|
||||
hasMessages: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
defineEmits(['reset', 'close']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-2 border-b border-n-weak h-12"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2 flex-1">
|
||||
<span class="font-medium text-sm text-n-slate-12">
|
||||
{{ $t('CAPTAIN.COPILOT.TITLE') }}
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
v-if="hasMessages"
|
||||
icon="i-lucide-plus"
|
||||
ghost
|
||||
sm
|
||||
@click="$emit('reset')"
|
||||
/>
|
||||
<Button icon="i-lucide-x" ghost sm @click="$emit('close')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -13,19 +13,16 @@ const sendMessage = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
class="border border-n-weak bg-n-alpha-3 rounded-lg h-12 flex"
|
||||
@submit.prevent="sendMessage"
|
||||
>
|
||||
<form class="relative" @submit.prevent="sendMessage">
|
||||
<input
|
||||
v-model="message"
|
||||
type="text"
|
||||
:placeholder="$t('CAPTAIN.COPILOT.SEND_MESSAGE')"
|
||||
class="w-full reset-base bg-transparent px-4 py-3 text-n-slate-11 text-sm"
|
||||
class="w-full reset-base bg-n-alpha-3 ltr:pl-4 ltr:pr-12 rtl:pl-12 rtl:pr-4 py-3 text-n-slate-11 text-sm border border-n-weak rounded-lg focus:outline-none focus:ring-1 focus:ring-n-blue-11 focus:border-n-blue-11"
|
||||
@keyup.enter="sendMessage"
|
||||
/>
|
||||
<button
|
||||
class="h-auto w-12 flex items-center justify-center text-n-slate-11"
|
||||
class="absolute ltr:right-1 rtl:left-1 top-1/2 -translate-y-1/2 h-9 w-10 flex items-center justify-center text-n-slate-11 hover:text-n-blue-11"
|
||||
type="submit"
|
||||
>
|
||||
<i class="i-ph-arrow-up" />
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRoute } from 'vue-router';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
|
||||
const getUIState = useMapGetter('uiState/getUIState');
|
||||
const isSidebarOpen = computed(() => getUIState.value('isCopilotSidebarOpen'));
|
||||
|
||||
const isConversationRoute = computed(() => {
|
||||
const CONVERSATION_ROUTES = [
|
||||
'inbox_conversation',
|
||||
'conversation_through_inbox',
|
||||
'conversations_through_label',
|
||||
'team_conversations_through_label',
|
||||
'conversations_through_folders',
|
||||
'conversation_through_mentions',
|
||||
'conversation_through_unattended',
|
||||
'conversation_through_participating',
|
||||
];
|
||||
return CONVERSATION_ROUTES.includes(route.name);
|
||||
});
|
||||
|
||||
const toggleSidebar = () => {
|
||||
store.dispatch('uiState/toggle', 'isCopilotSidebarOpen');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="fixed bottom-4 right-4 z-50">
|
||||
<div
|
||||
v-if="!isSidebarOpen && !isConversationRoute"
|
||||
class="rounded-full bg-n-solid-2 p-2 hover:bg-n-solid-3"
|
||||
>
|
||||
<Button
|
||||
icon="i-woot-captain"
|
||||
class="!rounded-full !bg-n-alpha-2 text-n-slate-12 text-xl"
|
||||
lg
|
||||
@click="toggleSidebar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
import Icon from '../../components-next/icon/Icon.vue';
|
||||
defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-2 p-3 rounded-lg bg-n-background/50 border border-n-weak hover:bg-n-background/80 transition-colors duration-200"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<Icon
|
||||
icon="i-lucide-sparkles"
|
||||
class="w-4 h-4 mt-0.5 flex-shrink-0 text-n-slate-9"
|
||||
/>
|
||||
<div class="text-sm text-n-slate-11">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import CopilotThinkingGroup from './CopilotThinkingGroup.vue';
|
||||
|
||||
const messages = [
|
||||
{
|
||||
id: 1,
|
||||
content: 'Analyzing the user query',
|
||||
reasoning: 'Breaking down the request into actionable steps',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: 'Searching codebase',
|
||||
reasoning: 'Looking for relevant files and functions',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: 'Generating response',
|
||||
reasoning: 'Composing a helpful and accurate answer',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Story title="Captain/Copilot/CopilotThinkingGroup" group="components">
|
||||
<Variant title="Default">
|
||||
<CopilotThinkingGroup :messages="messages" />
|
||||
</Variant>
|
||||
|
||||
<Variant title="With Default Collapsed">
|
||||
<!-- eslint-disable-next-line -->
|
||||
<CopilotThinkingGroup :messages="messages" :default-collapsed="true" />
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Icon from '../icon/Icon.vue';
|
||||
import CopilotThinkingBlock from './CopilotThinkingBlock.vue';
|
||||
|
||||
const props = defineProps({
|
||||
messages: { type: Array, required: true },
|
||||
defaultCollapsed: { type: Boolean, default: false },
|
||||
});
|
||||
const { t } = useI18n();
|
||||
const isExpanded = ref(!props.defaultCollapsed);
|
||||
|
||||
const thinkingCount = computed(() => props.messages.length);
|
||||
|
||||
watch(
|
||||
() => props.defaultCollapsed,
|
||||
newValue => {
|
||||
if (newValue) {
|
||||
isExpanded.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
class="group flex items-center gap-2 text-xs text-n-slate-10 hover:text-n-slate-11 transition-colors duration-200 -ml-3"
|
||||
@click="isExpanded = !isExpanded"
|
||||
>
|
||||
<Icon
|
||||
:icon="isExpanded ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
|
||||
class="w-4 h-4 transition-transform duration-200 group-hover:scale-110"
|
||||
/>
|
||||
<span class="flex items-center gap-2">
|
||||
{{ t('CAPTAIN.COPILOT.SHOW_STEPS') }}
|
||||
<span
|
||||
class="inline-flex items-center justify-center h-4 min-w-4 px-1 text-xs font-medium rounded-full bg-n-solid-3 text-n-slate-11"
|
||||
>
|
||||
{{ thinkingCount }}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
v-show="isExpanded"
|
||||
class="space-y-3 transition-all duration-200"
|
||||
:class="{
|
||||
'opacity-100': isExpanded,
|
||||
'opacity-0 max-h-0 overflow-hidden': !isExpanded,
|
||||
}"
|
||||
>
|
||||
<CopilotThinkingBlock
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
:content="message.content"
|
||||
:reasoning="message.reasoning"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -519,7 +519,7 @@ const menuItems = computed(() => {
|
||||
</div>
|
||||
</section>
|
||||
<nav class="grid flex-grow gap-2 px-2 pb-5 overflow-y-scroll no-scrollbar">
|
||||
<ul class="flex flex-col gap-2 m-0 list-none">
|
||||
<ul class="flex flex-col gap-1.5 m-0 list-none">
|
||||
<SidebarGroup
|
||||
v-for="item in menuItems"
|
||||
:key="item.name"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useImpersonation } from 'dashboard/composables/useImpersonation';
|
||||
|
||||
import {
|
||||
DropdownContainer,
|
||||
@@ -20,6 +21,8 @@ const currentUserAvailability = useMapGetter('getCurrentUserAvailability');
|
||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||
const currentUserAutoOffline = useMapGetter('getCurrentUserAutoOffline');
|
||||
|
||||
const { isImpersonating } = useImpersonation();
|
||||
|
||||
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
|
||||
const statusList = computed(() => {
|
||||
return [
|
||||
@@ -46,6 +49,10 @@ const activeStatus = computed(() => {
|
||||
});
|
||||
|
||||
function changeAvailabilityStatus(availability) {
|
||||
if (isImpersonating.value) {
|
||||
useAlert(t('PROFILE_SETTINGS.FORM.AVAILABILITY.IMPERSONATING_ERROR'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
store.dispatch('updateAvailability', {
|
||||
availability,
|
||||
|
||||
@@ -824,7 +824,7 @@ watch(conversationFilters, (newVal, oldVal) => {
|
||||
class="flex flex-col flex-shrink-0 bg-n-solid-1 conversations-list-wrap"
|
||||
:class="[
|
||||
{ hidden: !showConversationList },
|
||||
isOnExpandedLayout ? 'basis-full' : 'w-[360px] 2xl:w-[420px]',
|
||||
isOnExpandedLayout ? 'basis-full' : 'w-[21rem] 2xl:w-[26rem]',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -80,16 +80,14 @@ const toggleConversationLayout = () => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 px-4"
|
||||
class="flex items-center justify-between gap-2 px-3 h-12 pt-3 pb-3"
|
||||
:class="{
|
||||
'pb-3 border-b border-n-strong': hasAppliedFiltersOrActiveFolders,
|
||||
'pt-3 pb-2': showV4View,
|
||||
'mb-2 pb-0': !showV4View,
|
||||
'border-b border-n-strong': hasAppliedFiltersOrActiveFolders,
|
||||
}"
|
||||
>
|
||||
<div class="flex items-center justify-center min-w-0">
|
||||
<h1
|
||||
class="text-lg font-medium truncate text-n-slate-12"
|
||||
class="text-base font-medium truncate text-n-slate-12"
|
||||
:title="pageTitle"
|
||||
>
|
||||
{{ pageTitle }}
|
||||
|
||||
@@ -1,31 +1,24 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watchEffect } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import Copilot from 'dashboard/components-next/copilot/Copilot.vue';
|
||||
import ConversationAPI from 'dashboard/api/inbox/conversation';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
conversationInboxType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const assistants = useMapGetter('captainAssistants/getRecords');
|
||||
const inboxAssistant = useMapGetter('getCopilotAssistant');
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
|
||||
const getUIState = useMapGetter('uiState/getUIState');
|
||||
const isSidebarOpen = computed(() => getUIState.value('isCopilotSidebarOpen'));
|
||||
|
||||
const messages = ref([]);
|
||||
const isCaptainTyping = ref(false);
|
||||
const selectedAssistantId = ref(null);
|
||||
const copilotConnector = ref(null);
|
||||
|
||||
const activeAssistant = computed(() => {
|
||||
const preferredId = uiSettings.value.preferred_captain_assistant_id;
|
||||
@@ -59,7 +52,12 @@ const handleReset = () => {
|
||||
messages.value = [];
|
||||
};
|
||||
|
||||
const sendMessage = async message => {
|
||||
const sendMessage = message => {
|
||||
// Ensure WebSocket is connected before sending
|
||||
if (!copilotConnector.value || !isSidebarOpen.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add user message
|
||||
messages.value.push({
|
||||
id: messages.value.length + 1,
|
||||
@@ -68,55 +66,71 @@ const sendMessage = async message => {
|
||||
});
|
||||
isCaptainTyping.value = true;
|
||||
|
||||
try {
|
||||
const { data } = await ConversationAPI.requestCopilot(
|
||||
props.conversationId,
|
||||
{
|
||||
previous_history: messages.value
|
||||
.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
}))
|
||||
.slice(0, -1),
|
||||
message,
|
||||
assistant_id: selectedAssistantId.value,
|
||||
}
|
||||
);
|
||||
messages.value.push({
|
||||
id: new Date().getTime(),
|
||||
role: 'assistant',
|
||||
content: data.message,
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
console.log(error);
|
||||
} finally {
|
||||
isCaptainTyping.value = false;
|
||||
}
|
||||
copilotConnector.value.sendMessage({
|
||||
assistantId: activeAssistant.value.id,
|
||||
conversationId: currentChat.value?.id,
|
||||
previousHistory: messages.value
|
||||
.filter(m => m.role !== 'assistant_thinking')
|
||||
.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
}))
|
||||
.slice(0, -1),
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('captainAssistants/get');
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.conversationId) {
|
||||
store.dispatch('getInboxCaptainAssistantById', props.conversationId);
|
||||
selectedAssistantId.value = activeAssistant.value?.id;
|
||||
}
|
||||
});
|
||||
// const onCopilotResponse = data => {
|
||||
// if (data.type === 'final_response') {
|
||||
// messages.value.push({
|
||||
// id: new Date().getTime(),
|
||||
// role: 'assistant',
|
||||
// content: data.response.response,
|
||||
// });
|
||||
// isCaptainTyping.value = false;
|
||||
// } else if (data.type === 'ui_event') {
|
||||
// if (data.event === 'ui:linear_ticket_create') {
|
||||
// emitter.emit('ui:linear_ticket_create');
|
||||
// setTimeout(() => {
|
||||
// emitter.emit('ui:linear_ticket_create_data', data.response);
|
||||
// }, 100);
|
||||
// }
|
||||
// } else {
|
||||
// messages.value.push({
|
||||
// id: new Date().getTime(),
|
||||
// role: 'assistant_thinking',
|
||||
// content: data.response.response,
|
||||
// reasoning: data.response.reasoning,
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleClose = () => {
|
||||
store.dispatch('uiState/set', { isCopilotSidebarOpen: false });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Copilot
|
||||
:messages="messages"
|
||||
:support-agent="currentUser"
|
||||
:is-captain-typing="isCaptainTyping"
|
||||
:conversation-inbox-type="conversationInboxType"
|
||||
:assistants="assistants"
|
||||
:active-assistant="activeAssistant"
|
||||
@set-assistant="setAssistant"
|
||||
@send-message="sendMessage"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
<div
|
||||
v-if="isSidebarOpen"
|
||||
class="border-l border-n-weak w-[20rem] min-w-[20rem]"
|
||||
>
|
||||
<Copilot
|
||||
:messages="messages"
|
||||
:support-agent="currentUser"
|
||||
:is-captain-typing="isCaptainTyping"
|
||||
:conversation-inbox-type="conversationInboxType"
|
||||
:assistants="assistants"
|
||||
:active-assistant="activeAssistant"
|
||||
@set-assistant="setAssistant"
|
||||
@send-message="sendMessage"
|
||||
@reset="handleReset"
|
||||
@close="handleClose"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="hidden" />
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useImpersonation } from 'dashboard/composables/useImpersonation';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
|
||||
@@ -20,6 +21,10 @@ export default {
|
||||
AvailabilityStatusBadge,
|
||||
NextButton,
|
||||
},
|
||||
setup() {
|
||||
const { isImpersonating } = useImpersonation();
|
||||
return { isImpersonating };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isStatusMenuOpened: false,
|
||||
@@ -73,6 +78,13 @@ export default {
|
||||
});
|
||||
},
|
||||
changeAvailabilityStatus(availability) {
|
||||
if (this.isImpersonating) {
|
||||
useAlert(
|
||||
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.IMPERSONATING_ERROR')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ useKeyboardEvents(keyboardEvents);
|
||||
<template>
|
||||
<woot-tabs
|
||||
:index="activeTabIndex"
|
||||
class="w-full px-4 py-0 tab--chat-type"
|
||||
class="w-full px-3 -mt-1 py-0 tab--chat-type"
|
||||
@change="onTabChange"
|
||||
>
|
||||
<woot-tabs-item
|
||||
|
||||
@@ -83,7 +83,7 @@ export default {
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<div v-if="hasOpenedAtleastOnce" class="dashboard-app--container">
|
||||
<div v-if="hasOpenedAtleastOnce" class="flex-1">
|
||||
<div
|
||||
v-for="(configItem, index) in config"
|
||||
:key="index"
|
||||
@@ -115,6 +115,7 @@ export default {
|
||||
.dashboard-app--list iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.dashboard-app_loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -4,11 +4,9 @@ import ConversationHeader from './ConversationHeader.vue';
|
||||
import DashboardAppFrame from '../DashboardApp/Frame.vue';
|
||||
import EmptyState from './EmptyState/EmptyState.vue';
|
||||
import MessagesView from './MessagesView.vue';
|
||||
import ConversationSidebar from './ConversationSidebar.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConversationSidebar,
|
||||
ConversationHeader,
|
||||
DashboardAppFrame,
|
||||
EmptyState,
|
||||
@@ -25,16 +23,11 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isContactPanelOpen: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['contactPanelToggle'],
|
||||
data() {
|
||||
return { activeIndex: 0 };
|
||||
},
|
||||
@@ -57,9 +50,6 @@ export default {
|
||||
})),
|
||||
];
|
||||
},
|
||||
showContactPanel() {
|
||||
return this.isContactPanelOpen && this.currentChat.id;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'currentChat.inbox_id': {
|
||||
@@ -86,9 +76,6 @@ export default {
|
||||
}
|
||||
this.$store.dispatch('conversationLabels/get', this.currentChat.id);
|
||||
},
|
||||
onToggleContactPanel() {
|
||||
this.$emit('contactPanelToggle');
|
||||
},
|
||||
onDashboardAppTabChange(index) {
|
||||
this.activeIndex = index;
|
||||
},
|
||||
@@ -98,7 +85,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="conversation-details-wrap bg-n-background"
|
||||
class="conversation-details-wrap bg-n-background relative"
|
||||
:class="{
|
||||
'border-l rtl:border-l-0 rtl:border-r border-n-weak': !isOnExpandedLayout,
|
||||
}"
|
||||
@@ -107,14 +94,12 @@ export default {
|
||||
v-if="currentChat.id"
|
||||
:chat="currentChat"
|
||||
:is-inbox-view="isInboxView"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
:show-back-button="isOnExpandedLayout && !isInboxView"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
<woot-tabs
|
||||
v-if="dashboardApps.length && currentChat.id"
|
||||
:index="activeIndex"
|
||||
class="-mt-px bg-white dashboard-app--tabs dark:bg-slate-900"
|
||||
class="-mt-px border-t border-t-n-background bg-n-background dashboard-app--tabs"
|
||||
@change="onDashboardAppTabChange"
|
||||
>
|
||||
<woot-tabs-item
|
||||
@@ -125,23 +110,17 @@ export default {
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
<div v-show="!activeIndex" class="flex h-full min-h-0 m-0">
|
||||
<div v-show="!activeIndex" class="flex-1 h-full min-h-0 m-0">
|
||||
<MessagesView
|
||||
v-if="currentChat.id"
|
||||
:inbox-id="inboxId"
|
||||
:is-inbox-view="isInboxView"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
<EmptyState
|
||||
v-if="!currentChat.id && !isInboxView"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
/>
|
||||
<ConversationSidebar
|
||||
v-if="showContactPanel"
|
||||
:current-chat="currentChat"
|
||||
@toggle-contact-panel="onToggleContactPanel"
|
||||
/>
|
||||
<slot />
|
||||
</div>
|
||||
<DashboardAppFrame
|
||||
v-for="(dashboardApp, index) in dashboardApps"
|
||||
|
||||
@@ -243,7 +243,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-4 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group"
|
||||
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-3 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3 group"
|
||||
:class="{
|
||||
'active animate-card-select bg-n-alpha-1 dark:bg-n-alpha-3 border-n-weak':
|
||||
isActiveChat,
|
||||
@@ -278,7 +278,7 @@ export default {
|
||||
:badge="inboxBadge"
|
||||
:username="currentContact.name"
|
||||
:status="currentContact.availability_status"
|
||||
size="40px"
|
||||
size="32px"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import BackButton from '../BackButton.vue';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import InboxName from '../InboxName.vue';
|
||||
@@ -13,8 +12,6 @@ import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import Linear from './linear/index.vue';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BackButton,
|
||||
@@ -23,7 +20,6 @@ export default {
|
||||
Thumbnail,
|
||||
SLACardLabel,
|
||||
Linear,
|
||||
NextButton,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
@@ -31,10 +27,6 @@ export default {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
isContactPanelOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showBackButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -44,15 +36,6 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['contactPanelToggle'],
|
||||
setup(props, { emit }) {
|
||||
const keyboardEvents = {
|
||||
'Alt+KeyO': {
|
||||
action: () => emit('contactPanelToggle'),
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
@@ -99,13 +82,7 @@ export default {
|
||||
}
|
||||
return this.$t('CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_REPLY');
|
||||
},
|
||||
contactPanelToggleText() {
|
||||
return `${
|
||||
this.isContactPanelOpen
|
||||
? this.$t('CONVERSATION.HEADER.CLOSE')
|
||||
: this.$t('CONVERSATION.HEADER.OPEN')
|
||||
} ${this.$t('CONVERSATION.HEADER.DETAILS')}`;
|
||||
},
|
||||
|
||||
inbox() {
|
||||
const { inbox_id: inboxId } = this.chat;
|
||||
return this.$store.getters['inboxes/getInbox'](inboxId);
|
||||
@@ -133,7 +110,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-center justify-between px-4 py-2 border-b bg-n-background border-n-weak md:flex-row"
|
||||
class="flex flex-col items-center justify-between px-3 border-b bg-n-background border-n-weak md:flex-row h-12"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-center justify-center flex-1 w-full min-w-0"
|
||||
@@ -150,20 +127,18 @@ export default {
|
||||
:badge="inboxBadge"
|
||||
:username="currentContact.name"
|
||||
:status="currentContact.availability_status"
|
||||
size="32px"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-col items-start min-w-0 ml-2 overflow-hidden rtl:ml-0 rtl:mr-2 w-fit"
|
||||
>
|
||||
<div
|
||||
class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit"
|
||||
>
|
||||
<NextButton link slate @click.prevent="$emit('contactPanelToggle')">
|
||||
<span
|
||||
class="text-base font-medium truncate leading-tight text-n-slate-12"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
</span>
|
||||
</NextButton>
|
||||
<div class="flex items-center max-w-full gap-1 p-0 m-0 w-fit">
|
||||
<span
|
||||
class="text-sm font-medium truncate leading-tight text-n-slate-12"
|
||||
>
|
||||
{{ currentContact.name }}
|
||||
</span>
|
||||
|
||||
<fluent-icon
|
||||
v-if="!isHMACVerified"
|
||||
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
|
||||
@@ -180,19 +155,11 @@ export default {
|
||||
<span v-if="isSnoozed" class="font-medium text-n-amber-10">
|
||||
{{ snoozedDisplayText }}
|
||||
</span>
|
||||
<NextButton
|
||||
link
|
||||
xs
|
||||
blue
|
||||
:label="contactPanelToggleText"
|
||||
@click="$emit('contactPanelToggle')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-row items-center justify-end flex-grow gap-2 mt-3 header-actions-wrap lg:mt-0"
|
||||
:class="{ 'justify-end': isContactPanelOpen }"
|
||||
>
|
||||
<SLACardLabel v-if="hasSlaPolicyId" :chat="chat" show-extended-info />
|
||||
<Linear
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import CopilotContainer from '../../copilot/CopilotContainer.vue';
|
||||
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel.vue';
|
||||
import TabBar from 'dashboard/components-next/tabbar/TabBar.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { computed } from 'vue';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
|
||||
const props = defineProps({
|
||||
currentChat: {
|
||||
@@ -14,69 +11,48 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['toggleContactPanel']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const channelType = computed(() => props.currentChat?.meta?.channel || '');
|
||||
|
||||
const CONTACT_TABS_OPTIONS = [
|
||||
{ key: 'CONTACT', value: 'contact' },
|
||||
{ key: 'COPILOT', value: 'copilot' },
|
||||
];
|
||||
|
||||
const tabs = computed(() => {
|
||||
return CONTACT_TABS_OPTIONS.map(tab => ({
|
||||
label: t(`CONVERSATION.SIDEBAR.${tab.key}`),
|
||||
value: tab.value,
|
||||
}));
|
||||
});
|
||||
const activeTab = ref(0);
|
||||
const toggleContactPanel = () => {
|
||||
emit('toggleContactPanel');
|
||||
};
|
||||
|
||||
const handleTabChange = selectedTab => {
|
||||
activeTab.value = tabs.value.findIndex(
|
||||
tabItem => tabItem.value === selectedTab.value
|
||||
);
|
||||
};
|
||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||
const isFeatureEnabledonAccount = useMapGetter(
|
||||
'accounts/isFeatureEnabledonAccount'
|
||||
const getUIState = useMapGetter('uiState/getUIState');
|
||||
const isCopilotSidebarOpen = computed(() =>
|
||||
getUIState.value('isCopilotSidebarOpen')
|
||||
);
|
||||
const isConversationSidebarOpen = computed(() =>
|
||||
getUIState.value('isConversationSidebarOpen')
|
||||
);
|
||||
|
||||
const showCopilotTab = computed(() =>
|
||||
isFeatureEnabledonAccount.value(currentAccountId.value, FEATURE_FLAGS.CAPTAIN)
|
||||
const showConversationSidebar = computed(
|
||||
() =>
|
||||
props.currentChat.id &&
|
||||
!isCopilotSidebarOpen.value &&
|
||||
isConversationSidebarOpen.value
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
const closeConversationPanel = () => {
|
||||
store.dispatch('uiState/set', { isConversationSidebarOpen: false });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 w-80 min-w-80 2xl:min-w-96 2xl:w-96 flex flex-col bg-n-background"
|
||||
v-if="showConversationSidebar"
|
||||
class="ltr:border-l rtl:border-r border-n-weak h-full overflow-hidden z-10 w-[20rem] min-w-[20rem] flex flex-col"
|
||||
>
|
||||
<div v-if="showCopilotTab" class="p-2">
|
||||
<TabBar
|
||||
:tabs="tabs"
|
||||
:initial-active-tab="activeTab"
|
||||
class="w-full [&>button]:w-full"
|
||||
@tab-changed="handleTabChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 overflow-auto">
|
||||
<div class="flex flex-1 flex-col overflow-auto">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 px-4 py-2 border-b border-n-weak h-12"
|
||||
>
|
||||
<span class="font-medium text-sm text-n-slate-12">
|
||||
{{ $t('CONVERSATION.SIDEBAR.ACTIONS') }}
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<Button icon="i-lucide-x" ghost sm @click="closeConversationPanel" />
|
||||
</div>
|
||||
</div>
|
||||
<ContactPanel
|
||||
v-if="!activeTab"
|
||||
:conversation-id="currentChat.id"
|
||||
:inbox-id="currentChat.inbox_id"
|
||||
:on-toggle="toggleContactPanel"
|
||||
/>
|
||||
<CopilotContainer
|
||||
v-else-if="activeTab === 1 && showCopilotTab"
|
||||
:key="currentChat.id"
|
||||
:conversation-inbox-type="channelType"
|
||||
:conversation-id="currentChat.id"
|
||||
class="flex-1"
|
||||
:conversation-id="props.currentChat.id"
|
||||
:inbox-id="props.currentChat.inbox_id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="hidden" />
|
||||
</template>
|
||||
|
||||
@@ -38,8 +38,6 @@ import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Message,
|
||||
@@ -47,20 +45,8 @@ export default {
|
||||
ReplyBox,
|
||||
Banner,
|
||||
ConversationLabelSuggestion,
|
||||
NextButton,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
isContactPanelOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isInboxView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['contactPanelToggle'],
|
||||
setup() {
|
||||
const isPopOutReplyBox = ref(false);
|
||||
const { isEnterprise } = useConfig();
|
||||
@@ -199,12 +185,6 @@ export default {
|
||||
isATweet() {
|
||||
return this.conversationType === 'tweet';
|
||||
},
|
||||
isRightOrLeftIcon() {
|
||||
if (this.isContactPanelOpen) {
|
||||
return 'arrow-chevron-right';
|
||||
}
|
||||
return 'arrow-chevron-left';
|
||||
},
|
||||
getLastSeenAt() {
|
||||
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
|
||||
return contactLastSeenAt;
|
||||
@@ -440,9 +420,6 @@ export default {
|
||||
relevantMessages
|
||||
);
|
||||
},
|
||||
onToggleContactPanel() {
|
||||
this.$emit('contactPanelToggle');
|
||||
},
|
||||
setScrollParams() {
|
||||
this.heightBeforeLoad = this.conversationPanel.scrollHeight;
|
||||
this.scrollTopBeforeLoad = this.conversationPanel.scrollTop;
|
||||
@@ -526,19 +503,7 @@ export default {
|
||||
class="mx-2 mt-2 overflow-hidden rounded-lg"
|
||||
:banner-message="$t('CONVERSATION.OLD_INSTAGRAM_INBOX_REPLY_BANNER')"
|
||||
/>
|
||||
<div class="flex justify-end">
|
||||
<NextButton
|
||||
faded
|
||||
xs
|
||||
slate
|
||||
class="!rounded-r-none rtl:rotate-180 !rounded-2xl !fixed z-10"
|
||||
:icon="
|
||||
isContactPanelOpen ? 'i-ph-caret-right-fill' : 'i-ph-caret-left-fill'
|
||||
"
|
||||
:class="isInboxView ? 'top-52 md:top-40' : 'top-32'"
|
||||
@click="onToggleContactPanel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NextMessageList
|
||||
v-if="showNextBubbles"
|
||||
class="conversation-panel"
|
||||
|
||||
@@ -10,6 +10,7 @@ import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import SearchableDropdown from './SearchableDropdown.vue';
|
||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useEmitter } from 'dashboard/composables/emitter';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -204,6 +205,12 @@ const createIssue = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
useEmitter('ui:linear_ticket_create_data', data => {
|
||||
formState.title = data.title;
|
||||
formState.description = data.description;
|
||||
formState.priority = data.priority;
|
||||
});
|
||||
|
||||
onMounted(getTeams);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useTrack } from 'dashboard/composables';
|
||||
import { LINEAR_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useEmitter } from 'dashboard/composables/emitter';
|
||||
|
||||
const props = defineProps({
|
||||
conversationId: {
|
||||
@@ -94,6 +95,10 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
useEmitter('ui:linear_ticket_create', () => {
|
||||
shouldShowPopup.value = true;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
loadLinkedIssue();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { useImpersonation } from '../useImpersonation';
|
||||
|
||||
vi.mock('shared/helpers/sessionStorage', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||
|
||||
describe('useImpersonation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return true if impersonation flag is set in session storage', () => {
|
||||
SessionStorage.get.mockReturnValue(true);
|
||||
const { isImpersonating } = useImpersonation();
|
||||
expect(isImpersonating.value).toBe(true);
|
||||
expect(SessionStorage.get).toHaveBeenCalledWith(
|
||||
SESSION_STORAGE_KEYS.IMPERSONATION_USER
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false if impersonation flag is not set in session storage', () => {
|
||||
SessionStorage.get.mockReturnValue(false);
|
||||
const { isImpersonating } = useImpersonation();
|
||||
expect(isImpersonating.value).toBe(false);
|
||||
expect(SessionStorage.get).toHaveBeenCalledWith(
|
||||
SESSION_STORAGE_KEYS.IMPERSONATION_USER
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { computed } from 'vue';
|
||||
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||
|
||||
export function useImpersonation() {
|
||||
const isImpersonating = computed(() => {
|
||||
return SessionStorage.get(SESSION_STORAGE_KEYS.IMPERSONATION_USER);
|
||||
});
|
||||
return { isImpersonating };
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const SESSION_STORAGE_KEYS = {
|
||||
IMPERSONATION_USER: 'impersonationUser',
|
||||
};
|
||||
@@ -59,6 +59,11 @@
|
||||
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
||||
}
|
||||
},
|
||||
"ACCESS_TOKEN": {
|
||||
"TITLE": "Access Token",
|
||||
"DESCRIPTION": "Copy the access token and save it securely",
|
||||
"COPY_SUCCESSFUL": "Access token copied to clipboard"
|
||||
},
|
||||
"FORM": {
|
||||
"AVATAR": {
|
||||
"LABEL": "Bot avatar"
|
||||
|
||||
@@ -458,6 +458,10 @@
|
||||
"PLACEHOLDER": "Add Twitter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE_CONTACT": {
|
||||
"MESSAGE": "This action is permanent and irreversible.",
|
||||
"BUTTON": "Delete now"
|
||||
}
|
||||
},
|
||||
"DETAILS": {
|
||||
@@ -467,7 +471,7 @@
|
||||
"DELETE_CONTACT": "Delete contact",
|
||||
"DELETE_DIALOG": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
"DESCRIPTION": "Are you sure you want to delete this {contactName} contact?",
|
||||
"DESCRIPTION": "Are you sure you want to delete this contact?",
|
||||
"CONFIRM": "Yes, Delete",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
||||
|
||||
@@ -234,8 +234,7 @@
|
||||
}
|
||||
},
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contact",
|
||||
"COPILOT": "Copilot"
|
||||
"ACTIONS": "Actions"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
@@ -380,9 +379,6 @@
|
||||
"TWO": "{user} and {secondUser} are typing",
|
||||
"MULTIPLE": "{user} and {count} others are typing"
|
||||
},
|
||||
"COPILOT": {
|
||||
"TRY_THESE_PROMPTS": "Try these prompts"
|
||||
},
|
||||
"GALLERY_VIEW": {
|
||||
"ERROR_DOWNLOADING": "Unable to download attachment. Please try again"
|
||||
}
|
||||
|
||||
@@ -327,6 +327,10 @@
|
||||
"NAME": "Captain",
|
||||
"HEADER_KNOW_MORE": "Know more",
|
||||
"COPILOT": {
|
||||
"TITLE": "Copilot",
|
||||
"TRY_THESE_PROMPTS": "Try these prompts",
|
||||
"PANEL_TITLE": "Get started with Copilot",
|
||||
"KICK_OFF_MESSAGE": "Need a quick summary, want to check past conversations, or draft a better reply? Copilot’s here to speed things up.",
|
||||
"SEND_MESSAGE": "Send message...",
|
||||
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
|
||||
"LOADER": "Captain is thinking",
|
||||
@@ -334,6 +338,7 @@
|
||||
"USE": "Use this",
|
||||
"RESET": "Reset",
|
||||
"SELECT_ASSISTANT": "Select Assistant",
|
||||
"SHOW_STEPS": "Show steps",
|
||||
"PROMPTS": {
|
||||
"SUMMARIZE": {
|
||||
"LABEL": "Summarize this conversation",
|
||||
|
||||
@@ -185,7 +185,8 @@
|
||||
"OFFLINE": "Offline"
|
||||
},
|
||||
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
|
||||
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again"
|
||||
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again",
|
||||
"IMPERSONATING_ERROR": "Cannot change availability while impersonating a user"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Your email address",
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<script>
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
ref,
|
||||
computed,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
nextTick,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import NextSidebar from 'next/sidebar/Sidebar.vue';
|
||||
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
|
||||
@@ -9,6 +18,8 @@ import AccountSelector from 'dashboard/components/layout/sidebarComponents/Accou
|
||||
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
|
||||
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
||||
import UpgradePage from 'dashboard/routes/dashboard/upgrade/UpgradePage.vue';
|
||||
import CopilotContainer from 'dashboard/components/copilot/CopilotContainer.vue';
|
||||
import CopilotLauncher from 'dashboard/components-next/copilot/CopilotLauncher.vue';
|
||||
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
@@ -16,178 +27,160 @@ import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
const CommandBar = defineAsyncComponent(
|
||||
() => import('./commands/commandbar.vue')
|
||||
);
|
||||
|
||||
const Sidebar = defineAsyncComponent(
|
||||
() => import('../../components/layout/Sidebar.vue')
|
||||
);
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextSidebar,
|
||||
Sidebar,
|
||||
CommandBar,
|
||||
WootKeyShortcutModal,
|
||||
AddAccountModal,
|
||||
AccountSelector,
|
||||
AddLabelModal,
|
||||
NotificationPanel,
|
||||
UpgradePage,
|
||||
},
|
||||
setup() {
|
||||
const upgradePageRef = ref(null);
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const { accountId } = useAccount();
|
||||
const route = useRoute();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
accountId,
|
||||
upgradePageRef,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showAccountModal: false,
|
||||
showCreateAccountModal: false,
|
||||
showAddLabelModal: false,
|
||||
showShortcutModal: false,
|
||||
isNotificationPanel: false,
|
||||
displayLayoutType: '',
|
||||
hasBanner: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
currentRoute() {
|
||||
return ' ';
|
||||
},
|
||||
showUpgradePage() {
|
||||
return this.upgradePageRef?.shouldShowUpgradePage;
|
||||
},
|
||||
bypassUpgradePage() {
|
||||
return [
|
||||
'billing_settings_index',
|
||||
'settings_inbox_list',
|
||||
'agent_list',
|
||||
].includes(this.$route.name);
|
||||
},
|
||||
isSidebarOpen() {
|
||||
const { show_secondary_sidebar: showSecondarySidebar } = this.uiSettings;
|
||||
return showSecondarySidebar;
|
||||
},
|
||||
previouslyUsedDisplayType() {
|
||||
const {
|
||||
previously_used_conversation_display_type: conversationDisplayType,
|
||||
} = this.uiSettings;
|
||||
return conversationDisplayType;
|
||||
},
|
||||
previouslyUsedSidebarView() {
|
||||
const { previously_used_sidebar_view: showSecondarySidebar } =
|
||||
this.uiSettings;
|
||||
return showSecondarySidebar;
|
||||
},
|
||||
showNextSidebar() {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.CHATWOOT_V4
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
displayLayoutType() {
|
||||
const { LAYOUT_TYPES } = wootConstants;
|
||||
this.updateUISettings({
|
||||
conversation_display_type:
|
||||
this.displayLayoutType === LAYOUT_TYPES.EXPANDED
|
||||
? LAYOUT_TYPES.EXPANDED
|
||||
: this.previouslyUsedDisplayType,
|
||||
show_secondary_sidebar:
|
||||
this.displayLayoutType === LAYOUT_TYPES.EXPANDED
|
||||
? false
|
||||
: this.previouslyUsedSidebarView,
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.handleResize();
|
||||
this.$nextTick(this.checkBanner);
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
window.addEventListener('resize', this.checkBanner);
|
||||
emitter.on(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
window.removeEventListener('resize', this.checkBanner);
|
||||
emitter.off(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
},
|
||||
const upgradePageRef = ref(null);
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const { currentAccount } = useAccount();
|
||||
|
||||
methods: {
|
||||
checkBanner() {
|
||||
this.hasBanner =
|
||||
document.getElementsByClassName('woot-banner').length > 0;
|
||||
},
|
||||
handleResize() {
|
||||
const { SMALL_SCREEN_BREAKPOINT, LAYOUT_TYPES } = wootConstants;
|
||||
let throttled = false;
|
||||
const delay = 150;
|
||||
const showAccountModal = ref(false);
|
||||
const showCreateAccountModal = ref(false);
|
||||
const showAddLabelModal = ref(false);
|
||||
const showShortcutModal = ref(false);
|
||||
const isNotificationPanel = ref(false);
|
||||
const displayLayoutType = ref('');
|
||||
const hasBanner = ref('');
|
||||
|
||||
if (throttled) {
|
||||
return;
|
||||
}
|
||||
throttled = true;
|
||||
const isFeatureEnabledonAccount = useMapGetter(
|
||||
'accounts/isFeatureEnabledonAccount'
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
throttled = false;
|
||||
if (window.innerWidth <= SMALL_SCREEN_BREAKPOINT) {
|
||||
this.displayLayoutType = LAYOUT_TYPES.EXPANDED;
|
||||
} else {
|
||||
this.displayLayoutType = LAYOUT_TYPES.CONDENSED;
|
||||
}
|
||||
}, delay);
|
||||
},
|
||||
toggleSidebar() {
|
||||
this.updateUISettings({
|
||||
show_secondary_sidebar: !this.isSidebarOpen,
|
||||
previously_used_sidebar_view: !this.isSidebarOpen,
|
||||
});
|
||||
},
|
||||
openCreateAccountModal() {
|
||||
this.showAccountModal = false;
|
||||
this.showCreateAccountModal = true;
|
||||
},
|
||||
closeCreateAccountModal() {
|
||||
this.showCreateAccountModal = false;
|
||||
},
|
||||
toggleAccountModal() {
|
||||
this.showAccountModal = !this.showAccountModal;
|
||||
},
|
||||
toggleKeyShortcutModal() {
|
||||
this.showShortcutModal = true;
|
||||
},
|
||||
closeKeyShortcutModal() {
|
||||
this.showShortcutModal = false;
|
||||
},
|
||||
showAddLabelPopup() {
|
||||
this.showAddLabelModal = true;
|
||||
},
|
||||
hideAddLabelPopup() {
|
||||
this.showAddLabelModal = false;
|
||||
},
|
||||
openNotificationPanel() {
|
||||
this.isNotificationPanel = true;
|
||||
},
|
||||
closeNotificationPanel() {
|
||||
this.isNotificationPanel = false;
|
||||
},
|
||||
},
|
||||
const currentRoute = computed(() => ' ');
|
||||
|
||||
const showUpgradePage = computed(
|
||||
() => upgradePageRef.value?.shouldShowUpgradePage
|
||||
);
|
||||
|
||||
const bypassUpgradePage = computed(() =>
|
||||
['billing_settings_index', 'settings_inbox_list', 'agent_list'].includes(
|
||||
route.name
|
||||
)
|
||||
);
|
||||
|
||||
const isSidebarOpen = computed(() => {
|
||||
const { show_secondary_sidebar: showSecondarySidebar } = uiSettings;
|
||||
return showSecondarySidebar;
|
||||
});
|
||||
|
||||
const previouslyUsedDisplayType = computed(() => {
|
||||
const { previously_used_conversation_display_type: conversationDisplayType } =
|
||||
uiSettings;
|
||||
return conversationDisplayType;
|
||||
});
|
||||
|
||||
const previouslyUsedSidebarView = computed(() => {
|
||||
const { previously_used_sidebar_view: showSecondarySidebar } = uiSettings;
|
||||
return showSecondarySidebar;
|
||||
});
|
||||
|
||||
const showNextSidebar = computed(() =>
|
||||
isFeatureEnabledonAccount.value(
|
||||
currentAccount.value?.id,
|
||||
FEATURE_FLAGS.CHATWOOT_V4
|
||||
)
|
||||
);
|
||||
|
||||
const checkBanner = () => {
|
||||
hasBanner.value = document.getElementsByClassName('woot-banner').length > 0;
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
const { SMALL_SCREEN_BREAKPOINT, LAYOUT_TYPES } = wootConstants;
|
||||
let throttled = false;
|
||||
const delay = 150;
|
||||
|
||||
if (throttled) return;
|
||||
throttled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
throttled = false;
|
||||
displayLayoutType.value =
|
||||
window.innerWidth <= SMALL_SCREEN_BREAKPOINT
|
||||
? LAYOUT_TYPES.EXPANDED
|
||||
: LAYOUT_TYPES.CONDENSED;
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
updateUISettings({
|
||||
show_secondary_sidebar: !isSidebarOpen.value,
|
||||
previously_used_sidebar_view: !isSidebarOpen.value,
|
||||
});
|
||||
};
|
||||
|
||||
const openCreateAccountModal = () => {
|
||||
showAccountModal.value = false;
|
||||
showCreateAccountModal.value = true;
|
||||
};
|
||||
|
||||
const closeCreateAccountModal = () => {
|
||||
showCreateAccountModal.value = false;
|
||||
};
|
||||
|
||||
const toggleAccountModal = () => {
|
||||
showAccountModal.value = !showAccountModal.value;
|
||||
};
|
||||
|
||||
const toggleKeyShortcutModal = () => {
|
||||
showShortcutModal.value = true;
|
||||
};
|
||||
|
||||
const closeKeyShortcutModal = () => {
|
||||
showShortcutModal.value = false;
|
||||
};
|
||||
|
||||
const showAddLabelPopup = () => {
|
||||
showAddLabelModal.value = true;
|
||||
};
|
||||
|
||||
const hideAddLabelPopup = () => {
|
||||
showAddLabelModal.value = false;
|
||||
};
|
||||
|
||||
const openNotificationPanel = () => {
|
||||
isNotificationPanel.value = true;
|
||||
};
|
||||
|
||||
const closeNotificationPanel = () => {
|
||||
isNotificationPanel.value = false;
|
||||
};
|
||||
|
||||
watch(displayLayoutType, () => {
|
||||
const { LAYOUT_TYPES } = wootConstants;
|
||||
updateUISettings({
|
||||
conversation_display_type:
|
||||
displayLayoutType.value === LAYOUT_TYPES.EXPANDED
|
||||
? LAYOUT_TYPES.EXPANDED
|
||||
: previouslyUsedDisplayType.value,
|
||||
show_secondary_sidebar:
|
||||
displayLayoutType.value === LAYOUT_TYPES.EXPANDED
|
||||
? false
|
||||
: previouslyUsedSidebarView.value,
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
handleResize();
|
||||
nextTick(checkBanner);
|
||||
window.addEventListener('resize', handleResize);
|
||||
window.addEventListener('resize', checkBanner);
|
||||
emitter.on(BUS_EVENTS.TOGGLE_SIDEMENU, toggleSidebar);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('resize', checkBanner);
|
||||
emitter.off(BUS_EVENTS.TOGGLE_SIDEMENU, toggleSidebar);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -218,6 +211,8 @@ export default {
|
||||
/>
|
||||
<template v-if="!showUpgradePage">
|
||||
<router-view />
|
||||
<CopilotLauncher />
|
||||
<CopilotContainer />
|
||||
<CommandBar />
|
||||
<NotificationPanel
|
||||
v-if="isNotificationPanel"
|
||||
|
||||
@@ -29,10 +29,6 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
onToggle: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -89,8 +85,6 @@ watch(conversationId, (newConversationId, prevConversationId) => {
|
||||
|
||||
watch(contactId, getContactDetails);
|
||||
|
||||
const onPanelToggle = props.onToggle;
|
||||
|
||||
const onDragEnd = () => {
|
||||
dragging.value = false;
|
||||
updateUISettings({
|
||||
@@ -107,11 +101,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<ContactInfo
|
||||
:contact="contact"
|
||||
:channel-type="channelType"
|
||||
@toggle-panel="onPanelToggle"
|
||||
/>
|
||||
<ContactInfo :contact="contact" :channel-type="channelType" />
|
||||
<div class="list-group pb-8">
|
||||
<Draggable
|
||||
:list="conversationSidebarItems"
|
||||
|
||||
@@ -10,13 +10,17 @@ import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import CmdBarConversationSnooze from 'dashboard/routes/dashboard/commands/CmdBarConversationSnooze.vue';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import ConversationSidebar from 'dashboard/components/widgets/conversation/ConversationSidebar.vue';
|
||||
import PanelButtons from './PanelButtons.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChatList,
|
||||
ConversationBox,
|
||||
ConversationSidebar,
|
||||
PopOverSearch,
|
||||
CmdBarConversationSnooze,
|
||||
PanelButtons,
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
// Clear selected state if navigating away from a conversation to a route without a conversationId to prevent stale data issues
|
||||
@@ -87,14 +91,6 @@ export default {
|
||||
this.uiSettings;
|
||||
return conversationDisplayType !== CONDENSED;
|
||||
},
|
||||
isContactPanelOpen() {
|
||||
if (this.currentChat.id) {
|
||||
const { is_contact_sidebar_open: isContactSidebarOpen } =
|
||||
this.uiSettings;
|
||||
return isContactSidebarOpen;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
showPopOverSearch() {
|
||||
return !this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
@@ -189,11 +185,7 @@ export default {
|
||||
this.$store.dispatch('clearSelectedState');
|
||||
}
|
||||
},
|
||||
onToggleContactPanel() {
|
||||
this.updateUISettings({
|
||||
is_contact_sidebar_open: !this.isContactPanelOpen,
|
||||
});
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
this.showSearchModal = true;
|
||||
},
|
||||
@@ -225,10 +217,11 @@ export default {
|
||||
<ConversationBox
|
||||
v-if="showMessageView"
|
||||
:inbox-id="inboxId"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
>
|
||||
<PanelButtons v-if="currentChat.id" />
|
||||
</ConversationBox>
|
||||
<ConversationSidebar :current-chat="currentChat" />
|
||||
<CmdBarConversationSnooze />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const handleConversationSidebarToggle = () => {
|
||||
store.dispatch('uiState/set', {
|
||||
isConversationSidebarOpen: true,
|
||||
isCopilotSidebarOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopilotSidebarToggle = () => {
|
||||
store.dispatch('uiState/set', {
|
||||
isCopilotSidebarOpen: true,
|
||||
isConversationSidebarOpen: false,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-center items-center absolute top-24 right-1 bg-n-alpha-2 border border-n-weak rounded-lg bg-n-background gap-2 py-0.5 px-0.5"
|
||||
>
|
||||
<Button
|
||||
ghost
|
||||
slate
|
||||
sm
|
||||
class="!text-sm"
|
||||
icon="i-ph-user-bold"
|
||||
@click="handleConversationSidebarToggle"
|
||||
/>
|
||||
<Button
|
||||
ghost
|
||||
class="!text-sm !text-n-iris-11"
|
||||
sm
|
||||
icon="i-woot-captain"
|
||||
@click="handleCopilotSidebarToggle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
+131
-45
@@ -5,12 +5,15 @@ import { useAlert } from 'dashboard/composables';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { required, helpers, url } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import Input from 'dashboard/components-next/input/Input.vue';
|
||||
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
import AccessToken from 'dashboard/routes/dashboard/settings/profile/AccessToken.vue';
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
@@ -42,6 +45,9 @@ const formState = reactive({
|
||||
botAvatarUrl: '',
|
||||
});
|
||||
|
||||
const [showAccessToken, toggleAccessToken] = useToggle();
|
||||
const accessToken = ref('');
|
||||
|
||||
const v$ = useVuelidate(
|
||||
{
|
||||
botName: {
|
||||
@@ -70,11 +76,22 @@ const isLoading = computed(() =>
|
||||
: uiFlags.value.isUpdating
|
||||
);
|
||||
|
||||
const dialogTitle = computed(() =>
|
||||
props.type === MODAL_TYPES.CREATE
|
||||
const dialogTitle = computed(() => {
|
||||
if (showAccessToken.value) {
|
||||
return t('AGENT_BOTS.ACCESS_TOKEN.TITLE');
|
||||
}
|
||||
|
||||
return props.type === MODAL_TYPES.CREATE
|
||||
? t('AGENT_BOTS.ADD.TITLE')
|
||||
: t('AGENT_BOTS.EDIT.TITLE')
|
||||
);
|
||||
: t('AGENT_BOTS.EDIT.TITLE');
|
||||
});
|
||||
|
||||
const dialogDescription = computed(() => {
|
||||
if (showAccessToken.value) {
|
||||
return t('AGENT_BOTS.ACCESS_TOKEN.DESCRIPTION');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const confirmButtonLabel = computed(() =>
|
||||
props.type === MODAL_TYPES.CREATE
|
||||
@@ -90,6 +107,13 @@ const botUrlError = computed(() =>
|
||||
v$.value.botUrl.$error ? v$.value.botUrl.$errors[0]?.$message : ''
|
||||
);
|
||||
|
||||
const showAccessTokenInput = computed(
|
||||
() =>
|
||||
showAccessToken.value ||
|
||||
props.type === MODAL_TYPES.EDIT ||
|
||||
accessToken.value
|
||||
);
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(formState, {
|
||||
botName: '',
|
||||
@@ -128,6 +152,7 @@ const handleAvatarDelete = async () => {
|
||||
const handleSubmit = async () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
if (showAccessToken.value) return;
|
||||
|
||||
const botData = {
|
||||
name: formState.botName,
|
||||
@@ -144,7 +169,7 @@ const handleSubmit = async () => {
|
||||
? botData
|
||||
: { id: props.selectedBot.id, data: botData };
|
||||
|
||||
await store.dispatch(
|
||||
const response = await store.dispatch(
|
||||
`agentBots/${isCreate ? 'create' : 'update'}`,
|
||||
actionPayload
|
||||
);
|
||||
@@ -154,7 +179,21 @@ const handleSubmit = async () => {
|
||||
: t('AGENT_BOTS.EDIT.API.SUCCESS_MESSAGE');
|
||||
useAlert(alertKey);
|
||||
|
||||
dialogRef.value.close();
|
||||
// Show access token after creation
|
||||
if (isCreate) {
|
||||
const { access_token: responseAccessToken, id } = response || {};
|
||||
|
||||
if (id && responseAccessToken) {
|
||||
accessToken.value = responseAccessToken;
|
||||
toggleAccessToken(true);
|
||||
} else {
|
||||
accessToken.value = '';
|
||||
dialogRef.value.close();
|
||||
}
|
||||
} else {
|
||||
dialogRef.value.close();
|
||||
}
|
||||
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
const errorKey = isCreate
|
||||
@@ -166,17 +205,43 @@ const handleSubmit = async () => {
|
||||
|
||||
const initializeForm = () => {
|
||||
if (props.selectedBot && Object.keys(props.selectedBot).length) {
|
||||
const { name, description, outgoing_url, thumbnail, bot_config } =
|
||||
props.selectedBot;
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
outgoing_url: botUrl,
|
||||
thumbnail,
|
||||
bot_config: botConfig,
|
||||
access_token: botAccessToken,
|
||||
} = props.selectedBot;
|
||||
formState.botName = name || '';
|
||||
formState.botDescription = description || '';
|
||||
formState.botUrl = outgoing_url || bot_config?.webhook_url || '';
|
||||
formState.botUrl = botUrl || botConfig?.webhook_url || '';
|
||||
formState.botAvatarUrl = thumbnail || '';
|
||||
|
||||
if (botAccessToken && props.type === MODAL_TYPES.EDIT) {
|
||||
accessToken.value = botAccessToken;
|
||||
}
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
const onCopyToken = async value => {
|
||||
await copyTextToClipboard(value);
|
||||
useAlert(t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
if (!showAccessToken.value) v$.value?.$reset();
|
||||
accessToken.value = '';
|
||||
toggleAccessToken(false);
|
||||
};
|
||||
|
||||
const onClickClose = () => {
|
||||
closeModal();
|
||||
dialogRef.value.close();
|
||||
};
|
||||
|
||||
watch(() => props.selectedBot, initializeForm, { immediate: true, deep: true });
|
||||
|
||||
defineExpose({ dialogRef });
|
||||
@@ -187,48 +252,68 @@ defineExpose({ dialogRef });
|
||||
ref="dialogRef"
|
||||
type="edit"
|
||||
:title="dialogTitle"
|
||||
:description="dialogDescription"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
@close="v$.$reset()"
|
||||
@close="closeModal"
|
||||
>
|
||||
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||
<div class="mb-2 flex flex-col items-start">
|
||||
<span class="mb-2 text-sm font-medium text-n-slate-12">
|
||||
{{ $t('AGENT_BOTS.FORM.AVATAR.LABEL') }}
|
||||
</span>
|
||||
<Avatar
|
||||
:src="formState.botAvatarUrl"
|
||||
:name="formState.botName"
|
||||
:size="68"
|
||||
allow-upload
|
||||
@upload="handleImageUpload"
|
||||
@delete="handleAvatarDelete"
|
||||
<div
|
||||
v-if="!showAccessToken || type === MODAL_TYPES.EDIT"
|
||||
class="flex flex-col gap-4"
|
||||
>
|
||||
<div class="mb-2 flex flex-col items-start">
|
||||
<span class="mb-2 text-sm font-medium text-n-slate-12">
|
||||
{{ $t('AGENT_BOTS.FORM.AVATAR.LABEL') }}
|
||||
</span>
|
||||
<Avatar
|
||||
:src="formState.botAvatarUrl"
|
||||
:name="formState.botName"
|
||||
:size="68"
|
||||
allow-upload
|
||||
icon-name="i-lucide-bot-message-square"
|
||||
@upload="handleImageUpload"
|
||||
@delete="handleAvatarDelete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
id="bot-name"
|
||||
v-model="formState.botName"
|
||||
:label="$t('AGENT_BOTS.FORM.NAME.LABEL')"
|
||||
:placeholder="$t('AGENT_BOTS.FORM.NAME.PLACEHOLDER')"
|
||||
:message="botNameError"
|
||||
:message-type="botNameError ? 'error' : 'info'"
|
||||
@blur="v$.botName.$touch()"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
id="bot-description"
|
||||
v-model="formState.botDescription"
|
||||
:label="$t('AGENT_BOTS.FORM.DESCRIPTION.LABEL')"
|
||||
:placeholder="$t('AGENT_BOTS.FORM.DESCRIPTION.PLACEHOLDER')"
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="bot-url"
|
||||
v-model="formState.botUrl"
|
||||
:label="$t('AGENT_BOTS.FORM.WEBHOOK_URL.LABEL')"
|
||||
:placeholder="$t('AGENT_BOTS.FORM.WEBHOOK_URL.PLACEHOLDER')"
|
||||
:message="botUrlError"
|
||||
:message-type="botUrlError ? 'error' : 'info'"
|
||||
@blur="v$.botUrl.$touch()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
v-model="formState.botName"
|
||||
:label="$t('AGENT_BOTS.FORM.NAME.LABEL')"
|
||||
:placeholder="$t('AGENT_BOTS.FORM.NAME.PLACEHOLDER')"
|
||||
:message="botNameError"
|
||||
:message-type="botNameError ? 'error' : 'info'"
|
||||
@blur="v$.botName.$touch()"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
v-model="formState.botDescription"
|
||||
:label="$t('AGENT_BOTS.FORM.DESCRIPTION.LABEL')"
|
||||
:placeholder="$t('AGENT_BOTS.FORM.DESCRIPTION.PLACEHOLDER')"
|
||||
/>
|
||||
|
||||
<Input
|
||||
v-model="formState.botUrl"
|
||||
:label="$t('AGENT_BOTS.FORM.WEBHOOK_URL.LABEL')"
|
||||
:placeholder="$t('AGENT_BOTS.FORM.WEBHOOK_URL.PLACEHOLDER')"
|
||||
:message="botUrlError"
|
||||
:message-type="botUrlError ? 'error' : 'info'"
|
||||
@blur="v$.botUrl.$touch()"
|
||||
/>
|
||||
<div v-if="showAccessTokenInput" class="flex flex-col gap-1">
|
||||
<label
|
||||
v-if="type === MODAL_TYPES.EDIT"
|
||||
class="mb-0.5 text-sm font-medium text-n-slate-12"
|
||||
>
|
||||
{{ $t('AGENT_BOTS.ACCESS_TOKEN.TITLE') }}
|
||||
</label>
|
||||
<AccessToken :value="accessToken" @on-copy="onCopyToken" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
@@ -236,9 +321,10 @@ defineExpose({ dialogRef });
|
||||
slate
|
||||
type="reset"
|
||||
:label="$t('AGENT_BOTS.FORM.CANCEL')"
|
||||
@click="dialogRef.close()"
|
||||
@click="onClickClose()"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="!showAccessToken"
|
||||
type="submit"
|
||||
data-testid="label-submit"
|
||||
:label="confirmButtonLabel"
|
||||
|
||||
@@ -39,6 +39,7 @@ const onClick = () => {
|
||||
<template #masked>
|
||||
<button
|
||||
class="absolute top-1.5 ltr:right-0.5 rtl:left-0.5"
|
||||
type="button"
|
||||
@click="toggleMasked"
|
||||
>
|
||||
<fluent-icon :icon="maskIcon" :size="16" />
|
||||
@@ -46,7 +47,7 @@ const onClick = () => {
|
||||
</template>
|
||||
</woot-input>
|
||||
<FormButton
|
||||
type="submit"
|
||||
type="button"
|
||||
size="large"
|
||||
icon="text-copy"
|
||||
variant="outline"
|
||||
|
||||
@@ -51,6 +51,7 @@ import captainDocuments from './captain/document';
|
||||
import captainResponses from './captain/response';
|
||||
import captainInboxes from './captain/inboxes';
|
||||
import captainBulkActions from './captain/bulkActions';
|
||||
import uiState from './modules/uiState';
|
||||
const plugins = [];
|
||||
|
||||
export default createStore({
|
||||
@@ -106,6 +107,7 @@ export default createStore({
|
||||
captainResponses,
|
||||
captainInboxes,
|
||||
captainBulkActions,
|
||||
uiState,
|
||||
},
|
||||
plugins,
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@ import types from '../mutation-types';
|
||||
import authAPI from '../../api/auth';
|
||||
|
||||
import { setUser, clearCookiesOnLogout } from '../utils/api';
|
||||
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||
|
||||
const initialState = {
|
||||
currentUser: {
|
||||
@@ -145,8 +147,15 @@ export const actions = {
|
||||
updateUISettings: async ({ commit }, params) => {
|
||||
try {
|
||||
commit(types.SET_CURRENT_USER_UI_SETTINGS, params);
|
||||
const response = await authAPI.updateUISettings(params);
|
||||
commit(types.SET_CURRENT_USER, response.data);
|
||||
|
||||
const isImpersonating = SessionStorage.get(
|
||||
SESSION_STORAGE_KEYS.IMPERSONATION_USER
|
||||
);
|
||||
|
||||
if (!isImpersonating) {
|
||||
const response = await authAPI.updateUISettings(params);
|
||||
commit(types.SET_CURRENT_USER, response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const initialState = {
|
||||
isCopilotSidebarOpen: false,
|
||||
isConversationSidebarOpen: true,
|
||||
};
|
||||
|
||||
const types = {
|
||||
SET_UI_STATE: 'SET_UI_STATE',
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
[types.SET_UI_STATE](state, { key, value }) {
|
||||
state[key] = value;
|
||||
},
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getUIState: state => key => state[key],
|
||||
};
|
||||
|
||||
const actions = {
|
||||
toggle({ commit, state }, key) {
|
||||
commit(types.SET_UI_STATE, { key, value: !state[key] });
|
||||
},
|
||||
set({ commit }, payload) {
|
||||
Object.entries(payload).forEach(([key, value]) => {
|
||||
commit(types.SET_UI_STATE, { key, value });
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: initialState,
|
||||
getters,
|
||||
mutations,
|
||||
actions,
|
||||
};
|
||||
@@ -2,7 +2,9 @@ import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
import differenceInDays from 'date-fns/differenceInDays';
|
||||
import Cookies from 'js-cookie';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import {
|
||||
ANALYTICS_IDENTITY,
|
||||
@@ -44,6 +46,10 @@ export const clearLocalStorageOnLogout = () => {
|
||||
LocalStorage.remove(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES);
|
||||
};
|
||||
|
||||
export const clearSessionStorageOnLogout = () => {
|
||||
SessionStorage.remove(SESSION_STORAGE_KEYS.IMPERSONATION_USER);
|
||||
};
|
||||
|
||||
export const deleteIndexedDBOnLogout = async () => {
|
||||
let dbs = [];
|
||||
try {
|
||||
@@ -75,6 +81,7 @@ export const clearCookiesOnLogout = () => {
|
||||
emitter.emit(ANALYTICS_RESET);
|
||||
clearBrowserSessionCookies();
|
||||
clearLocalStorageOnLogout();
|
||||
clearSessionStorageOnLogout();
|
||||
const globalConfig = window.globalConfig || {};
|
||||
const logoutRedirectLink = globalConfig.LOGOUT_REDIRECT_LINK || '/';
|
||||
window.location = logoutRedirectLink;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
export default {
|
||||
clearAll() {
|
||||
window.sessionStorage.clear();
|
||||
},
|
||||
|
||||
get(key) {
|
||||
try {
|
||||
const value = window.sessionStorage.getItem(key);
|
||||
return value ? JSON.parse(value) : null;
|
||||
} catch (error) {
|
||||
return window.sessionStorage.getItem(key);
|
||||
}
|
||||
},
|
||||
|
||||
set(key, value) {
|
||||
if (typeof value === 'object') {
|
||||
window.sessionStorage.setItem(key, JSON.stringify(value));
|
||||
} else {
|
||||
window.sessionStorage.setItem(key, value);
|
||||
}
|
||||
},
|
||||
|
||||
remove(key) {
|
||||
window.sessionStorage.removeItem(key);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
import SessionStorage from '../sessionStorage';
|
||||
|
||||
// Mocking sessionStorage
|
||||
const sessionStorageMock = (() => {
|
||||
let store = {};
|
||||
return {
|
||||
getItem: key => store[key] || null,
|
||||
setItem: (key, value) => {
|
||||
store[key] = String(value);
|
||||
},
|
||||
removeItem: key => delete store[key],
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, 'sessionStorage', {
|
||||
value: sessionStorageMock,
|
||||
});
|
||||
|
||||
describe('SessionStorage utility', () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
describe('clearAll method', () => {
|
||||
it('should clear all items from sessionStorage', () => {
|
||||
sessionStorage.setItem('testKey1', 'testValue1');
|
||||
sessionStorage.setItem('testKey2', 'testValue2');
|
||||
|
||||
SessionStorage.clearAll();
|
||||
|
||||
expect(sessionStorage.getItem('testKey1')).toBeNull();
|
||||
expect(sessionStorage.getItem('testKey2')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get method', () => {
|
||||
it('should retrieve and parse JSON values correctly', () => {
|
||||
const testObject = { a: 1, b: 'test' };
|
||||
sessionStorage.setItem('testKey', JSON.stringify(testObject));
|
||||
|
||||
expect(SessionStorage.get('testKey')).toEqual(testObject);
|
||||
});
|
||||
|
||||
it('should return null for non-existent keys', () => {
|
||||
expect(SessionStorage.get('nonExistentKey')).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle non-JSON values by returning the raw value', () => {
|
||||
sessionStorage.setItem('testKey', 'plain string value');
|
||||
|
||||
expect(SessionStorage.get('testKey')).toBe('plain string value');
|
||||
});
|
||||
|
||||
it('should handle malformed JSON gracefully', () => {
|
||||
sessionStorage.setItem('testKey', '{malformed:json}');
|
||||
|
||||
expect(SessionStorage.get('testKey')).toBe('{malformed:json}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('set method', () => {
|
||||
it('should store object values as JSON strings', () => {
|
||||
const testObject = { a: 1, b: 'test' };
|
||||
SessionStorage.set('testKey', testObject);
|
||||
|
||||
expect(sessionStorage.getItem('testKey')).toBe(
|
||||
JSON.stringify(testObject)
|
||||
);
|
||||
});
|
||||
|
||||
it('should store primitive values directly', () => {
|
||||
SessionStorage.set('stringKey', 'test string');
|
||||
expect(sessionStorage.getItem('stringKey')).toBe('test string');
|
||||
|
||||
SessionStorage.set('numberKey', 42);
|
||||
expect(sessionStorage.getItem('numberKey')).toBe('42');
|
||||
|
||||
SessionStorage.set('booleanKey', true);
|
||||
expect(sessionStorage.getItem('booleanKey')).toBe('true');
|
||||
});
|
||||
|
||||
it('should handle null values', () => {
|
||||
SessionStorage.set('nullKey', null);
|
||||
|
||||
expect(sessionStorage.getItem('nullKey')).toBe('null');
|
||||
expect(SessionStorage.get('nullKey')).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle undefined values', () => {
|
||||
SessionStorage.set('undefinedKey', undefined);
|
||||
|
||||
expect(sessionStorage.getItem('undefinedKey')).toBe('undefined');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove method', () => {
|
||||
it('should remove an item from sessionStorage', () => {
|
||||
SessionStorage.set('testKey', 'testValue');
|
||||
expect(SessionStorage.get('testKey')).toBe('testValue');
|
||||
|
||||
SessionStorage.remove('testKey');
|
||||
|
||||
expect(SessionStorage.get('testKey')).toBeNull();
|
||||
});
|
||||
|
||||
it('should do nothing when removing a non-existent key', () => {
|
||||
expect(() => {
|
||||
SessionStorage.remove('nonExistentKey');
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration of methods', () => {
|
||||
it('should set, get, and remove values correctly', () => {
|
||||
SessionStorage.set('testKey', { value: 'test' });
|
||||
|
||||
expect(SessionStorage.get('testKey')).toEqual({ value: 'test' });
|
||||
|
||||
SessionStorage.remove('testKey');
|
||||
expect(SessionStorage.get('testKey')).toBeNull();
|
||||
});
|
||||
|
||||
it('should correctly handle impersonation flag (common use case)', () => {
|
||||
SessionStorage.set('impersonationUser', true);
|
||||
|
||||
expect(SessionStorage.get('impersonationUser')).toBe(true);
|
||||
|
||||
expect(sessionStorage.getItem('impersonationUser')).toBe('true');
|
||||
|
||||
SessionStorage.remove('impersonationUser');
|
||||
expect(SessionStorage.get('impersonationUser')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,8 @@ import { parseBoolean } from '@chatwoot/utils';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { required, email } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
|
||||
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||
// mixins
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
|
||||
@@ -21,6 +22,8 @@ const ERROR_MESSAGES = {
|
||||
'business-account-only': 'LOGIN.OAUTH.BUSINESS_ACCOUNTS_ONLY',
|
||||
};
|
||||
|
||||
const IMPERSONATION_URL_SEARCH_KEY = 'impersonation';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormInput,
|
||||
@@ -112,6 +115,14 @@ export default {
|
||||
this.loginApi.message = message;
|
||||
useAlert(this.loginApi.message);
|
||||
},
|
||||
handleImpersonation() {
|
||||
// Detects impersonation mode via URL and sets a session flag to prevent user settings changes during impersonation.
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const impersonation = urlParams.get(IMPERSONATION_URL_SEARCH_KEY);
|
||||
if (impersonation) {
|
||||
SessionStorage.set(SESSION_STORAGE_KEYS.IMPERSONATION_USER, true);
|
||||
}
|
||||
},
|
||||
submitLogin() {
|
||||
this.loginApi.hasErrored = false;
|
||||
this.loginApi.showLoading = true;
|
||||
@@ -128,6 +139,7 @@ export default {
|
||||
|
||||
login(credentials)
|
||||
.then(() => {
|
||||
this.handleImpersonation();
|
||||
this.showAlertMessage(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
|
||||
})
|
||||
.catch(response => {
|
||||
|
||||
@@ -31,7 +31,7 @@ class ApplicationMailbox < ActionMailbox::Base
|
||||
end
|
||||
|
||||
def in_reply_to_matches?(in_reply_to)
|
||||
Array.wrap(in_reply_to).any? { _1.match?(CONVERSATION_MESSAGE_ID_PATTERN) }
|
||||
Array.wrap(in_reply_to).any? { it.match?(CONVERSATION_MESSAGE_ID_PATTERN) }
|
||||
end
|
||||
|
||||
# checks if follow this pattern send it to reply_mailbox
|
||||
|
||||
@@ -97,8 +97,8 @@ class Account < ApplicationRecord
|
||||
|
||||
has_one_attached :contacts_export
|
||||
|
||||
enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h
|
||||
enum status: { active: 0, suspended: 1 }
|
||||
enum :locale, LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h, prefix: true
|
||||
enum :status, { active: 0, suspended: 1 }
|
||||
|
||||
scope :with_auto_resolve, -> { where("(settings ->> 'auto_resolve_after')::int IS NOT NULL") }
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ module SsoAuthenticatable
|
||||
"#{ENV.fetch('FRONTEND_URL', nil)}/app/login?email=#{encoded_email}&sso_auth_token=#{generate_sso_auth_token}"
|
||||
end
|
||||
|
||||
def generate_sso_link_with_impersonation
|
||||
"#{generate_sso_link}&impersonation=true"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sso_token_key(token)
|
||||
|
||||
@@ -128,6 +128,12 @@ class Conversation < ApplicationRecord
|
||||
additional_attributes&.dig('conversation_language')
|
||||
end
|
||||
|
||||
# Be aware: The precision of created_at and last_activity_at may differ from Ruby's Time precision.
|
||||
# Our DB column (see schema) stores timestamps with second-level precision (no microseconds), so
|
||||
# if you assign a Ruby Time with microseconds, the DB will truncate it. This may cause subtle differences
|
||||
# if you compare or copy these values in Ruby, also in our specs
|
||||
# So in specs rely on to be_with(1.second) instead of to eq()
|
||||
# TODO: Migrate to use a timestamp with microsecond precision
|
||||
def last_activity_at
|
||||
self[:last_activity_at] || created_at
|
||||
end
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
# index_email_templates_on_name_and_account_id (name,account_id) UNIQUE
|
||||
#
|
||||
class EmailTemplate < ApplicationRecord
|
||||
enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h
|
||||
enum template_type: { layout: 0, content: 1 }
|
||||
enum :locale, LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h, prefix: true
|
||||
enum :template_type, { layout: 0, content: 1 }
|
||||
belongs_to :account, optional: true
|
||||
|
||||
validates :name, uniqueness: { scope: :account }
|
||||
|
||||
@@ -19,7 +19,7 @@ class InstallationConfig < ApplicationRecord
|
||||
# https://discuss.rubyonrails.org/t/cve-2022-32224-possible-rce-escalation-bug-with-serialized-columns-in-active-record/81017
|
||||
# FIX ME : fixes breakage of installation config. we need to migrate.
|
||||
# Fix configuration in application.rb
|
||||
serialize :serialized_value, ActiveSupport::HashWithIndifferentAccess
|
||||
serialize :serialized_value, coder: YAML, type: ActiveSupport::HashWithIndifferentAccess
|
||||
|
||||
before_validation :set_lock
|
||||
validates :name, presence: true
|
||||
@@ -32,6 +32,10 @@ class InstallationConfig < ApplicationRecord
|
||||
after_commit :clear_cache
|
||||
|
||||
def value
|
||||
# This is an extra hack again cause of the YAML serialization, in case of new object initialization in super admin
|
||||
# It was throwing error as the default value of column '{}' was failing in deserialization.
|
||||
return {}.with_indifferent_access if new_record? && @attributes['serialized_value']&.value_before_type_cast == '{}'
|
||||
|
||||
serialized_value[:value]
|
||||
end
|
||||
|
||||
|
||||
@@ -185,7 +185,13 @@ class Message < ApplicationRecord
|
||||
# move this to a presenter
|
||||
return self[:content] if !input_csat? || inbox.web_widget?
|
||||
|
||||
I18n.t('conversations.survey.response', link: "#{ENV.fetch('FRONTEND_URL', nil)}/survey/responses/#{conversation.uuid}")
|
||||
survey_link = "#{ENV.fetch('FRONTEND_URL', nil)}/survey/responses/#{conversation.uuid}"
|
||||
|
||||
if inbox.csat_config&.dig('message').present?
|
||||
"#{inbox.csat_config['message']} #{survey_link}"
|
||||
else
|
||||
I18n.t('conversations.survey.response', link: survey_link)
|
||||
end
|
||||
end
|
||||
|
||||
def email_notifiable_message?
|
||||
|
||||
+2
-2
@@ -109,8 +109,8 @@ class User < ApplicationRecord
|
||||
self.email = email.try(:downcase)
|
||||
end
|
||||
|
||||
def send_devise_notification(notification, *args)
|
||||
devise_mailer.with(account: Current.account).send(notification, self, *args).deliver_later
|
||||
def send_devise_notification(notification, *)
|
||||
devise_mailer.with(account: Current.account).send(notification, self, *).deliver_later
|
||||
end
|
||||
|
||||
def set_password_and_uid
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Account::SignUpEmailValidationService
|
||||
include CustomExceptions::Account
|
||||
attr_reader :email
|
||||
|
||||
def initialize(email)
|
||||
@email = email
|
||||
end
|
||||
|
||||
def perform
|
||||
address = ValidEmail2::Address.new(email)
|
||||
|
||||
raise InvalidEmail.new({ valid: false, disposable: nil }) unless address.valid?
|
||||
|
||||
raise InvalidEmail.new({ domain_blocked: true }) if domain_blocked?
|
||||
|
||||
raise InvalidEmail.new({ valid: true, disposable: true }) if address.disposable?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def domain_blocked?
|
||||
domain = email.split('@').last&.downcase
|
||||
blocked_domains.any? { |blocked_domain| domain.match?(blocked_domain.downcase) }
|
||||
end
|
||||
|
||||
def blocked_domains
|
||||
domains = GlobalConfigService.load('BLOCKED_EMAIL_DOMAINS', '')
|
||||
return [] if domains.blank?
|
||||
|
||||
domains.split("\n").map(&:strip)
|
||||
end
|
||||
end
|
||||
@@ -28,16 +28,6 @@ class Conversations::FilterService < FilterService
|
||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :messages, :contact_inbox
|
||||
)
|
||||
|
||||
account_user = @account.account_users.find_by(user_id: @user.id)
|
||||
is_administrator = account_user&.role == 'administrator'
|
||||
|
||||
# Ensure we only include conversations from inboxes the user has access to
|
||||
unless is_administrator
|
||||
inbox_ids = @user.inboxes.where(account_id: @account.id).pluck(:id)
|
||||
conversations = conversations.where(inbox_id: inbox_ids)
|
||||
end
|
||||
|
||||
# Apply permission-based filtering
|
||||
Conversations::PermissionFilterService.new(
|
||||
conversations,
|
||||
@user,
|
||||
|
||||
@@ -8,9 +8,23 @@ class Conversations::PermissionFilterService
|
||||
end
|
||||
|
||||
def perform
|
||||
# The base implementation simply returns all conversations
|
||||
# Enterprise edition extends this with permission-based filtering
|
||||
conversations
|
||||
return conversations if user_role == 'administrator'
|
||||
|
||||
accessible_conversations
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def accessible_conversations
|
||||
conversations.where(inbox: user.inboxes.where(account_id: account.id))
|
||||
end
|
||||
|
||||
def account_user
|
||||
AccountUser.find_by(account_id: account.id, user_id: user.id)
|
||||
end
|
||||
|
||||
def user_role
|
||||
account_user&.role
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -31,7 +31,11 @@ class Crm::Leadsquared::LeadFinderService
|
||||
def find_by_phone_number(contact)
|
||||
return if contact.phone_number.blank?
|
||||
|
||||
search_by_field(contact.phone_number)
|
||||
lead_data = Crm::Leadsquared::Mappers::ContactMapper.map(contact)
|
||||
|
||||
return if lead_data.blank? || lead_data['Mobile'].nil?
|
||||
|
||||
search_by_field(lead_data['Mobile'])
|
||||
end
|
||||
|
||||
def search_by_field(value)
|
||||
|
||||
@@ -20,11 +20,29 @@ class Crm::Leadsquared::Mappers::ContactMapper
|
||||
'FirstName' => contact.name.presence,
|
||||
'LastName' => contact.last_name.presence,
|
||||
'EmailAddress' => contact.email.presence,
|
||||
'Mobile' => contact.phone_number.presence,
|
||||
'Mobile' => formatted_phone_number,
|
||||
'Source' => brand_name
|
||||
}.compact
|
||||
end
|
||||
|
||||
def formatted_phone_number
|
||||
# it seems like leadsquared needs a different phone number format
|
||||
# it's not documented anywhere, so don't bother trying to look up online
|
||||
# After some trial and error, I figured out the format, its +<country_code>-<national_number>
|
||||
return nil if contact.phone_number.blank?
|
||||
|
||||
parsed = TelephoneNumber.parse(contact.phone_number)
|
||||
return contact.phone_number unless parsed.valid?
|
||||
|
||||
country_code = parsed.country.country_code
|
||||
e164 = parsed.e164_number
|
||||
e164 = e164.sub(/^\+/, '')
|
||||
|
||||
national_number = e164.sub(/^#{Regexp.escape(country_code)}/, '')
|
||||
|
||||
"+#{country_code}-#{national_number}"
|
||||
end
|
||||
|
||||
def brand_name
|
||||
::GlobalConfig.get('BRAND_NAME')['BRAND_NAME'] || 'Chatwoot'
|
||||
end
|
||||
|
||||
@@ -27,7 +27,7 @@ class MessageTemplates::HookExecutionService
|
||||
return false unless message.incoming?
|
||||
# prevents sending out-of-office message if an agent has sent a message in last 5 minutes
|
||||
# ensures better UX by not interrupting active conversations at the end of business hours
|
||||
return false if conversation.messages.outgoing.exists?(['created_at > ?', 5.minutes.ago])
|
||||
return false if conversation.messages.outgoing.where(private: false).exists?(['created_at > ?', 5.minutes.ago])
|
||||
|
||||
inbox.out_of_office? && conversation.messages.today.template.empty? && inbox.out_of_office_message.present?
|
||||
end
|
||||
|
||||
@@ -12,7 +12,6 @@ class MessageTemplates::Template::CsatSurvey
|
||||
private
|
||||
|
||||
delegate :contact, :account, :inbox, to: :conversation
|
||||
delegate :csat_config, to: :inbox
|
||||
|
||||
def should_send_csat_survey?
|
||||
return true unless survey_rules_configured?
|
||||
|
||||
@@ -137,14 +137,19 @@ class Twilio::IncomingMessageService
|
||||
end
|
||||
|
||||
def download_with_auth(media_url)
|
||||
Down.download(
|
||||
media_url,
|
||||
http_basic_authentication: [twilio_channel.account_sid, twilio_channel.auth_token || twilio_channel.api_key_sid]
|
||||
)
|
||||
auth_credentials = if twilio_channel.api_key_sid.present?
|
||||
# When using api_key_sid, the auth token should be the api_secret_key
|
||||
[twilio_channel.api_key_sid, twilio_channel.auth_token]
|
||||
else
|
||||
# When using account_sid, the auth token is the account's auth token
|
||||
[twilio_channel.account_sid, twilio_channel.auth_token]
|
||||
end
|
||||
|
||||
Down.download(media_url, http_basic_authentication: auth_credentials)
|
||||
end
|
||||
|
||||
def handle_download_attachment_error(error, media_url)
|
||||
Rails.logger.info "Error downloading attachment from Twilio: #{error.message}: Retrying"
|
||||
Rails.logger.info "Error downloading attachment from Twilio: #{error.message}: Retrying without auth"
|
||||
Down.download(media_url)
|
||||
rescue StandardError => e
|
||||
Rails.logger.info "Error downloading attachment from Twilio: #{e.message}: Skipping"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
json.access_token @resource.access_token.token
|
||||
json.expiry nil
|
||||
json.user do
|
||||
json.id @resource.id
|
||||
json.name @resource.name
|
||||
json.display_name @resource.display_name
|
||||
json.email @resource.email
|
||||
json.pubsub_token @resource.pubsub_token
|
||||
end
|
||||
@@ -2,7 +2,7 @@
|
||||
<hr/>
|
||||
<div class="form-actions">
|
||||
<p class="text-color-red">Caution: Any actions executed after impersonate will appear as if performed by the impersonated user - [<%= page.resource.name %> ]</p>
|
||||
<a class='button' target='_blank' href='<%= page.resource.generate_sso_link %>'>Impersonate user </a>
|
||||
<a class='button' target='_blank' href='<%= page.resource.generate_sso_link_with_impersonation %>'>Impersonate user </a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# TODO: Phase out the custom ConnectionPool wrappers ($alfred / $velma),
|
||||
# switch to plain Redis clients here and let Rails 7.1+ handle pooling
|
||||
# via `pool:` in RedisCacheStore (see rack_attack initializer).
|
||||
|
||||
# Alfred
|
||||
# Add here as you use it for more features
|
||||
# Used for Round Robin, Conversation Emails & Online Presence
|
||||
|
||||
@@ -14,6 +14,10 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
||||
if ActiveModel::Type::Boolean.new.cast(ENV.fetch('CW_API_ONLY_SERVER', false)) || Rails.env.development?
|
||||
resource '*', headers: :any, methods: :any, expose: %w[access-token client uid expiry]
|
||||
end
|
||||
|
||||
if ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_API_CORS', false))
|
||||
resource '/api/*', headers: :any, methods: :any, expose: %w[access-token client uid expiry]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -11,7 +11,12 @@ class Rack::Attack
|
||||
# Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
||||
|
||||
# https://github.com/rack/rack-attack/issues/102
|
||||
Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(redis: $velma)
|
||||
# Rails 7.1 automatically adds its own ConnectionPool around RedisCacheStore.
|
||||
# Because `$velma` is *already* a ConnectionPool, double-wrapping causes
|
||||
# Redis calls like `get` to hit the outer wrapper and explode.
|
||||
# `pool: false` tells Rails to skip its internal pool and use ours directly.
|
||||
# TODO: We can use build in connection pool in future upgrade
|
||||
Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(redis: $velma, pool: false)
|
||||
|
||||
class Request < ::Rack::Request
|
||||
# You many need to specify a method to fetch the correct remote IP address
|
||||
|
||||
+1
-1
@@ -127,7 +127,6 @@ Rails.application.routes.draw do
|
||||
post :unread
|
||||
post :custom_attributes
|
||||
get :attachments
|
||||
post :copilot
|
||||
get :inbox_assistant
|
||||
end
|
||||
end
|
||||
@@ -396,6 +395,7 @@ Rails.application.routes.draw do
|
||||
resources :users, only: [:create, :show, :update, :destroy] do
|
||||
member do
|
||||
get :login
|
||||
post :token
|
||||
end
|
||||
end
|
||||
resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
|
||||
|
||||
@@ -16,10 +16,10 @@ KillMode=mixed
|
||||
StandardInput=null
|
||||
SyslogIdentifier=%p
|
||||
|
||||
Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3/bin:/home/chatwoot/.rvm/gems/ruby-3.3.3@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.3.3/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin"
|
||||
Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.4.4/bin:/home/chatwoot/.rvm/gems/ruby-3.4.4@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.4.4/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin"
|
||||
Environment="PORT=3000"
|
||||
Environment="RAILS_ENV=production"
|
||||
Environment="NODE_ENV=production"
|
||||
Environment="RAILS_LOG_TO_STDOUT=true"
|
||||
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.3.3"
|
||||
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3:/home/chatwoot/.rvm/gems/ruby-3.3.3@global"
|
||||
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.4.4"
|
||||
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.4.4:/home/chatwoot/.rvm/gems/ruby-3.4.4@global"
|
||||
|
||||
@@ -21,10 +21,10 @@ MemoryHigh=1.4G
|
||||
MemorySwapMax=0
|
||||
OOMPolicy=stop
|
||||
|
||||
Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3/bin:/home/chatwoot/.rvm/gems/ruby-3.3.3@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.3.3/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin"
|
||||
Environment="PATH=/home/chatwoot/.rvm/gems/ruby-3.4.4/bin:/home/chatwoot/.rvm/gems/ruby-3.4.4@global/bin:/home/chatwoot/.rvm/rubies/ruby-3.4.4/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin"
|
||||
Environment="PORT=3000"
|
||||
Environment="RAILS_ENV=production"
|
||||
Environment="NODE_ENV=production"
|
||||
Environment="RAILS_LOG_TO_STDOUT=true"
|
||||
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.3.3"
|
||||
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3:/home/chatwoot/.rvm/gems/ruby-3.3.3@global"
|
||||
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.4.4"
|
||||
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.4.4:/home/chatwoot/.rvm/gems/ruby-3.4.4@global"
|
||||
|
||||
@@ -338,8 +338,8 @@ function setup_chatwoot() {
|
||||
sudo -i -u chatwoot << EOF
|
||||
rvm --version
|
||||
rvm autolibs disable
|
||||
rvm install "ruby-3.3.3"
|
||||
rvm use 3.3.3 --default
|
||||
rvm install "ruby-3.4.4"
|
||||
rvm use 3.4.4 --default
|
||||
|
||||
git clone https://github.com/chatwoot/chatwoot.git
|
||||
cd chatwoot
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
## Chatwoot Developer Documentation
|
||||
|
||||
Welcome to the official Chatwoot developer documentation. This guide contains everything you need to know about Chatwoot APIs and build custom flows on top of Chatwoot APIs.
|
||||
|
||||
### 👩💻 Development
|
||||
|
||||
Install the [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the documentation changes locally. To install, use the following command
|
||||
|
||||
```
|
||||
npm i -g mintlify
|
||||
```
|
||||
|
||||
Run the following command at the root of your documentation (where mint.json is)
|
||||
|
||||
```
|
||||
mintlify dev
|
||||
```
|
||||
|
||||
### 😎 Publishing Changes
|
||||
|
||||
Changes will be deployed to production automatically after pushing to the default branch.
|
||||
|
||||
You can also preview changes using PRs, which generates a preview link of the docs.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
- Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies.
|
||||
- Page loads as a 404 - Make sure you are running in a folder with `mint.json`
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"$schema": "https://mintlify.com/docs.json",
|
||||
"name": "Chatwoot Developer Docs",
|
||||
"description": "Official developer documentation for Chatwoot - the open-source customer support platform. Learn about our APIs, integrations, and development guidelines.",
|
||||
"logo": {
|
||||
"dark": "/logo/dark.png",
|
||||
"light": "/logo/light.png"
|
||||
},
|
||||
"favicon": "/favicon.png",
|
||||
"colors": {
|
||||
"primary": "#0069ED",
|
||||
"light": "#4D9CFF",
|
||||
"dark": "#0050B4"
|
||||
},
|
||||
"fonts": {
|
||||
"heading": {
|
||||
"family": "Haskoy",
|
||||
"weight": 500,
|
||||
"source": "https://d1j5ayohogpcd2.cloudfront.net/fonts/haskoy/Haskoy-Medium.woff2",
|
||||
"format": "woff2"
|
||||
},
|
||||
"body": {
|
||||
"family": "Haskoy",
|
||||
"weight": 400,
|
||||
"source": "https://d1j5ayohogpcd2.cloudfront.net/fonts/haskoy/Haskoy-Regular.woff2",
|
||||
"format": "woff2"
|
||||
}
|
||||
},
|
||||
"theme": "maple",
|
||||
"navigation": {
|
||||
"groups": [
|
||||
{
|
||||
"group": "API Reference",
|
||||
"pages": [
|
||||
"introduction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Application API",
|
||||
"description": "APIs for managing application aspects of Chatwoot",
|
||||
"includeTags": [
|
||||
"Agents",
|
||||
"Automation Rule",
|
||||
"Account AgentBots",
|
||||
"Canned Responses",
|
||||
"Contact Labels",
|
||||
"Contacts",
|
||||
"Conversation Assignments",
|
||||
"Conversation Labels",
|
||||
"Conversations",
|
||||
"Custom Attributes",
|
||||
"Custom Filters",
|
||||
"Help Center",
|
||||
"Inboxes",
|
||||
"Integrations",
|
||||
"Messages",
|
||||
"Profile",
|
||||
"Reports",
|
||||
"Teams",
|
||||
"Webhooks"
|
||||
],
|
||||
"openapi": "https://raw.githubusercontent.com/chatwoot/chatwoot/develop/swagger/tag_groups/application_swagger.json"
|
||||
},
|
||||
{
|
||||
"group": "Platform API",
|
||||
"description": "APIs for managing platform aspects of Chatwoot",
|
||||
"includeTags": [
|
||||
"Accounts",
|
||||
"Account Users",
|
||||
"AgentBots",
|
||||
"Users"
|
||||
],
|
||||
"openapi": "https://raw.githubusercontent.com/chatwoot/chatwoot/develop/swagger/tag_groups/platform_swagger.json"
|
||||
},
|
||||
{
|
||||
"group": "Client API",
|
||||
"description": "APIs for client applications",
|
||||
"includeTags": [
|
||||
"Contacts API",
|
||||
"Conversations API",
|
||||
"Messages API"
|
||||
],
|
||||
"openapi": "https://raw.githubusercontent.com/chatwoot/chatwoot/develop/swagger/tag_groups/client_swagger.json"
|
||||
},
|
||||
{
|
||||
"group": "Other APIs",
|
||||
"description": "Other Chatwoot APIs",
|
||||
"includeTags": [
|
||||
"CSAT Survey Page"
|
||||
],
|
||||
"openapi": "https://raw.githubusercontent.com/chatwoot/chatwoot/develop/swagger/tag_groups/other_swagger.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"footerSocials": {
|
||||
"twitter": "https://twitter.com/chatwootapp",
|
||||
"github": "https://github.com/chatwoot",
|
||||
"linkedin": "https://www.linkedin.com/company/chatwoot"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Introduction to Chatwoot APIs
|
||||
description: Learn how to use Chatwoot APIs to build integrations, customize chat experiences, and manage your installation.
|
||||
sidebarTitle: Introduction
|
||||
---
|
||||
|
||||
Welcome to the Chatwoot API documentation. Whether you're building custom workflows for your support team, integrating Chatwoot into your product, or managing users across installations, our APIs provide the flexibility and power to help you do more with Chatwoot.
|
||||
|
||||
Chatwoot provides three categories of APIs, each designed with a specific use case in mind:
|
||||
|
||||
- **Application APIs** – For account-level automation and agent-facing integrations.
|
||||
- **Client APIs** – For building custom chat interfaces for end-users
|
||||
- **Platform APIs** – For managing and administering installations at scale
|
||||
|
||||
---
|
||||
|
||||
## Application APIs
|
||||
|
||||
Application APIs are designed for interacting with a Chatwoot account from an agent/admin perspective. Use them to build internal tools, automate workflows, or perform bulk operations like data import/export.
|
||||
|
||||
- **Authentication**: Requires a user `access_token`, which can be generated from **Profile Settings** after logging into your Chatwoot account.
|
||||
- **Availability**: Supported on both **Cloud** and **Self-hosted** Chatwoot installations.
|
||||
- **Example**: [Google Cloud Functions Demo](https://github.com/chatwoot/google-cloud-functions-demo)
|
||||
|
||||
---
|
||||
|
||||
## Client APIs
|
||||
|
||||
Client APIs are intended for building custom messaging experiences over Chatwoot. If you're not using the native website widget or want to embed chat in your mobile app, these APIs are the way to go.
|
||||
|
||||
- **Authentication**: Uses `inbox_identifier` (from **Settings → Configuration** in API inboxes) and `contact_identifier` (returned when creating a contact).
|
||||
- **Availability**: Supported on both **Cloud** and **Self-hosted** Chatwoot installations.
|
||||
- **Examples**:
|
||||
|
||||
- [Client API Demo](https://github.com/chatwoot/client-api-demo)
|
||||
- [Flutter SDK](https://github.com/chatwoot/chatwoot-flutter-sdk)
|
||||
|
||||
---
|
||||
|
||||
## Platform APIs
|
||||
|
||||
Platform APIs are used to manage Chatwoot installations at the admin level. These APIs allow you to control users, roles, and accounts, or sync data from external authentication systems.
|
||||
|
||||
- **Authentication**: Requires an `access_token` generated by a **Platform App**, which can be created in the **Super Admin Console**.
|
||||
- **Availability**: Available on **Self-hosted** / **Managed Hosting** Chatwoot installations only.
|
||||
|
||||
---
|
||||
|
||||
Use the right API for your use case, and you'll be able to extend, customize, and integrate Chatwoot into your stack with ease.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
+4
-4
@@ -1,6 +1,6 @@
|
||||
# pre-build stage
|
||||
FROM node:23-alpine as node
|
||||
FROM ruby:3.3.3-alpine3.19 AS pre-builder
|
||||
FROM ruby:3.4.4-alpine3.21 AS pre-builder
|
||||
|
||||
ARG NODE_VERSION="23.7.0"
|
||||
ARG PNPM_VERSION="10.2.0"
|
||||
@@ -90,13 +90,13 @@ RUN if [ "$RAILS_ENV" = "production" ]; then \
|
||||
RUN git rev-parse HEAD > /app/.git_sha
|
||||
|
||||
# Remove unnecessary files
|
||||
RUN rm -rf /gems/ruby/3.3.0/cache/*.gem \
|
||||
&& find /gems/ruby/3.3.0/gems/ \( -name "*.c" -o -name "*.o" \) -delete \
|
||||
RUN rm -rf /gems/ruby/3.4.0/cache/*.gem \
|
||||
&& find /gems/ruby/3.4.0/gems/ \( -name "*.c" -o -name "*.o" \) -delete \
|
||||
&& rm -rf .git \
|
||||
&& rm .gitignore
|
||||
|
||||
# final build stage
|
||||
FROM ruby:3.3.3-alpine3.19
|
||||
FROM ruby:3.4.4-alpine3.21
|
||||
|
||||
ARG NODE_VERSION="23.7.0"
|
||||
ARG PNPM_VERSION="10.2.0"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user