Compare commits
44
Commits
v4.2.0
...
debug/mutex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e204c2aa4f | ||
|
|
1acc994d98 | ||
|
|
5185bbd34f | ||
|
|
58e5f7e4df | ||
|
|
7a5b170ff0 | ||
|
|
6c57dbb12d | ||
|
|
c174ee65e3 | ||
|
|
97b36a868e | ||
|
|
cb8821afbf | ||
|
|
cd71a6ec10 | ||
|
|
c1044a8b66 | ||
|
|
6e2a90113c | ||
|
|
9bd658137a | ||
|
|
f73c5ef0b8 | ||
|
|
f9fce5e2df | ||
|
|
c2d8e2ad77 | ||
|
|
03c0a7c62e | ||
|
|
d40a59f7fa | ||
|
|
72c5671e09 | ||
|
|
8c0885e1d2 | ||
|
|
e92f72b318 | ||
|
|
ea04a292ed | ||
|
|
deeb6d4e3f | ||
|
|
a3bf753f4c | ||
|
|
459f225559 | ||
|
|
99de5f4257 | ||
|
|
4b417ce9e7 | ||
|
|
8697a30dc5 | ||
|
|
8565341682 | ||
|
|
9bd7daeaf8 | ||
|
|
abe22c8649 | ||
|
|
743b73d0af | ||
|
|
dc7f1597e5 | ||
|
|
bc42aec68e | ||
|
|
3c8abd5b30 | ||
|
|
1602b071db | ||
|
|
4664402bf3 | ||
|
|
df7401f71c | ||
|
|
a07f2a7c1b | ||
|
|
af650af489 | ||
|
|
2ee63656e2 | ||
|
|
27ec791353 | ||
|
|
ccb3672ff8 | ||
|
|
54faefb876 |
+43
-43
@@ -73,15 +73,15 @@ jobs:
|
|||||||
libvips
|
libvips
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Install RVM and Ruby 3.3.3
|
name: Install RVM and Ruby 3.4.4
|
||||||
command: |
|
command: |
|
||||||
sudo apt-get install -y gpg
|
sudo apt-get install -y gpg
|
||||||
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
|
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
|
||||||
\curl -sSL https://get.rvm.io | bash -s stable
|
\curl -sSL https://get.rvm.io | bash -s stable
|
||||||
echo 'source ~/.rvm/scripts/rvm' >> $BASH_ENV
|
echo 'source ~/.rvm/scripts/rvm' >> $BASH_ENV
|
||||||
source ~/.rvm/scripts/rvm
|
source ~/.rvm/scripts/rvm
|
||||||
rvm install "3.3.3"
|
rvm install "3.4.4"
|
||||||
rvm use 3.3.3 --default
|
rvm use 3.4.4 --default
|
||||||
gem install bundler -v 2.5.16
|
gem install bundler -v 2.5.16
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
@@ -99,17 +99,17 @@ jobs:
|
|||||||
chmod +x ~/tmp/cc-test-reporter
|
chmod +x ~/tmp/cc-test-reporter
|
||||||
|
|
||||||
# Swagger verification
|
# Swagger verification
|
||||||
- run:
|
# - run:
|
||||||
name: Verify swagger API specification
|
# name: Verify swagger API specification
|
||||||
command: |
|
# command: |
|
||||||
bundle exec rake swagger:build
|
# bundle exec rake swagger:build
|
||||||
if [[ `git status swagger/swagger.json --porcelain` ]]
|
# if [[ `git status swagger/swagger.json --porcelain` ]]
|
||||||
then
|
# then
|
||||||
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
|
# echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
|
||||||
exit 1
|
# exit 1
|
||||||
fi
|
# fi
|
||||||
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
|
# curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
|
||||||
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
|
# java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
|
||||||
|
|
||||||
# we remove the FRONTED_URL from the .env before running the tests
|
# we remove the FRONTED_URL from the .env before running the tests
|
||||||
- run:
|
- run:
|
||||||
@@ -139,26 +139,26 @@ jobs:
|
|||||||
command: bundle exec bundle audit update && bundle exec bundle audit check -v
|
command: bundle exec bundle audit update && bundle exec bundle audit check -v
|
||||||
|
|
||||||
# Rubocop linting
|
# Rubocop linting
|
||||||
- run:
|
# - run:
|
||||||
name: Rubocop
|
# name: Rubocop
|
||||||
command: bundle exec rubocop
|
# command: bundle exec rubocop
|
||||||
|
|
||||||
# ESLint linting
|
# # ESLint linting
|
||||||
- run:
|
# - run:
|
||||||
name: eslint
|
# name: eslint
|
||||||
command: pnpm run eslint
|
# command: pnpm run eslint
|
||||||
|
|
||||||
- run:
|
# - run:
|
||||||
name: Run frontend tests
|
# name: Run frontend tests
|
||||||
command: |
|
# command: |
|
||||||
mkdir -p ~/build/coverage/frontend
|
# mkdir -p ~/build/coverage/frontend
|
||||||
~/tmp/cc-test-reporter before-build
|
# ~/tmp/cc-test-reporter before-build
|
||||||
pnpm run test:coverage
|
# pnpm run test:coverage
|
||||||
|
|
||||||
- run:
|
# - run:
|
||||||
name: Code Climate Test Coverage (Frontend)
|
# name: Code Climate Test Coverage (Frontend)
|
||||||
command: |
|
# command: |
|
||||||
~/tmp/cc-test-reporter format-coverage -t lcov -o "~/build/coverage/frontend/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
|
# ~/tmp/cc-test-reporter format-coverage -t lcov -o "~/build/coverage/frontend/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
|
||||||
|
|
||||||
# Run backend tests
|
# Run backend tests
|
||||||
- run:
|
- run:
|
||||||
@@ -175,17 +175,17 @@ jobs:
|
|||||||
-- ${TESTFILES}
|
-- ${TESTFILES}
|
||||||
no_output_timeout: 30m
|
no_output_timeout: 30m
|
||||||
|
|
||||||
- run:
|
# - run:
|
||||||
name: Code Climate Test Coverage (Backend)
|
# name: Code Climate Test Coverage (Backend)
|
||||||
command: |
|
# command: |
|
||||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o "~/build/coverage/backend/codeclimate.$CIRCLE_NODE_INDEX.json"
|
# ~/tmp/cc-test-reporter format-coverage -t simplecov -o "~/build/coverage/backend/codeclimate.$CIRCLE_NODE_INDEX.json"
|
||||||
|
|
||||||
- run:
|
# - run:
|
||||||
name: List coverage directory contents
|
# name: List coverage directory contents
|
||||||
command: |
|
# command: |
|
||||||
ls -R ~/build/coverage
|
# ls -R ~/build/coverage
|
||||||
|
|
||||||
- persist_to_workspace:
|
# - persist_to_workspace:
|
||||||
root: ~/build
|
# root: ~/build
|
||||||
paths:
|
# paths:
|
||||||
- coverage
|
# - coverage
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ services:
|
|||||||
args:
|
args:
|
||||||
VARIANT: 'ubuntu-22.04'
|
VARIANT: 'ubuntu-22.04'
|
||||||
NODE_VERSION: '23.7.0'
|
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.
|
# 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_UID: '1000'
|
||||||
USER_GID: '1000'
|
USER_GID: '1000'
|
||||||
@@ -25,7 +25,7 @@ services:
|
|||||||
args:
|
args:
|
||||||
VARIANT: 'ubuntu-22.04'
|
VARIANT: 'ubuntu-22.04'
|
||||||
NODE_VERSION: '23.7.0'
|
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.
|
# 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_UID: '1000'
|
||||||
USER_GID: '1000'
|
USER_GID: '1000'
|
||||||
|
|||||||
+179
-8
@@ -1,7 +1,10 @@
|
|||||||
require:
|
plugins:
|
||||||
- rubocop-performance
|
- rubocop-performance
|
||||||
- rubocop-rails
|
- rubocop-rails
|
||||||
- rubocop-rspec
|
- rubocop-rspec
|
||||||
|
- rubocop-factory_bot
|
||||||
|
|
||||||
|
require:
|
||||||
- ./rubocop/use_from_email.rb
|
- ./rubocop/use_from_email.rb
|
||||||
- ./rubocop/custom_cop_location.rb
|
- ./rubocop/custom_cop_location.rb
|
||||||
|
|
||||||
@@ -13,44 +16,61 @@ Metrics/ClassLength:
|
|||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/message.rb'
|
- 'app/models/message.rb'
|
||||||
- 'app/models/conversation.rb'
|
- 'app/models/conversation.rb'
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 19
|
Max: 19
|
||||||
|
Exclude:
|
||||||
|
- 'enterprise/lib/captain/agent.rb'
|
||||||
|
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 25
|
Max: 25
|
||||||
|
|
||||||
Style/Documentation:
|
Style/Documentation:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/ExponentialNotation:
|
Style/ExponentialNotation:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/FrozenStringLiteralComment:
|
Style/FrozenStringLiteralComment:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/SymbolArray:
|
Style/SymbolArray:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/OpenStructUse:
|
Style/OpenStructUse:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/OptionalBooleanParameter:
|
Style/OptionalBooleanParameter:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/services/email_templates/db_resolver_service.rb'
|
- 'app/services/email_templates/db_resolver_service.rb'
|
||||||
- 'app/dispatchers/dispatcher.rb'
|
- 'app/dispatchers/dispatcher.rb'
|
||||||
|
|
||||||
Style/GlobalVars:
|
Style/GlobalVars:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/initializers/01_redis.rb'
|
- 'config/initializers/01_redis.rb'
|
||||||
- 'config/initializers/rack_attack.rb'
|
- 'config/initializers/rack_attack.rb'
|
||||||
- 'lib/redis/alfred.rb'
|
- 'lib/redis/alfred.rb'
|
||||||
- 'lib/global_config.rb'
|
- 'lib/global_config.rb'
|
||||||
|
|
||||||
Style/ClassVars:
|
Style/ClassVars:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/services/email_templates/db_resolver_service.rb'
|
- 'app/services/email_templates/db_resolver_service.rb'
|
||||||
|
|
||||||
Lint/MissingSuper:
|
Lint/MissingSuper:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/drops/base_drop.rb'
|
- 'app/drops/base_drop.rb'
|
||||||
|
|
||||||
Lint/SymbolConversion:
|
Lint/SymbolConversion:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Lint/EmptyBlock:
|
Lint/EmptyBlock:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/views/api/v1/accounts/conversations/toggle_status.json.jbuilder'
|
- 'app/views/api/v1/accounts/conversations/toggle_status.json.jbuilder'
|
||||||
|
|
||||||
Lint/OrAssignmentToConstant:
|
Lint/OrAssignmentToConstant:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/redis/config.rb'
|
- 'lib/redis/config.rb'
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
Max: 30
|
Max: 30
|
||||||
Exclude:
|
Exclude:
|
||||||
@@ -58,10 +78,16 @@ Metrics/BlockLength:
|
|||||||
- '**/routes.rb'
|
- '**/routes.rb'
|
||||||
- 'config/environments/*'
|
- 'config/environments/*'
|
||||||
- db/schema.rb
|
- db/schema.rb
|
||||||
|
|
||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- lib/seeders/message_seeder.rb
|
- lib/seeders/message_seeder.rb
|
||||||
- spec/support/slack_stubs.rb
|
- spec/support/slack_stubs.rb
|
||||||
|
|
||||||
|
Rails/HelperInstanceVariable:
|
||||||
|
Exclude:
|
||||||
|
- enterprise/app/helpers/captain/chat_helper.rb
|
||||||
|
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
||||||
@@ -71,74 +97,101 @@ Rails/ApplicationController:
|
|||||||
- 'app/controllers/platform_controller.rb'
|
- 'app/controllers/platform_controller.rb'
|
||||||
- 'app/controllers/public_controller.rb'
|
- 'app/controllers/public_controller.rb'
|
||||||
- 'app/controllers/survey/responses_controller.rb'
|
- 'app/controllers/survey/responses_controller.rb'
|
||||||
|
|
||||||
Rails/FindEach:
|
Rails/FindEach:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Include:
|
Include:
|
||||||
- 'app/**/*.rb'
|
- 'app/**/*.rb'
|
||||||
|
|
||||||
Rails/CompactBlank:
|
Rails/CompactBlank:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Rails/EnvironmentVariableAccess:
|
Rails/EnvironmentVariableAccess:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Rails/TimeZoneAssignment:
|
Rails/TimeZoneAssignment:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Rails/RedundantPresenceValidationOnBelongsTo:
|
Rails/RedundantPresenceValidationOnBelongsTo:
|
||||||
Enabled: false
|
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:
|
Style/ClassAndModuleChildren:
|
||||||
EnforcedStyle: compact
|
EnforcedStyle: compact
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/application.rb'
|
- 'config/application.rb'
|
||||||
- 'config/initializers/monkey_patches/*'
|
- 'config/initializers/monkey_patches/*'
|
||||||
|
|
||||||
Style/MapToHash:
|
Style/MapToHash:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/HashSyntax:
|
Style/HashSyntax:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
EnforcedStyle: no_mixed_keys
|
EnforcedStyle: no_mixed_keys
|
||||||
EnforcedShorthandSyntax: never
|
EnforcedShorthandSyntax: never
|
||||||
|
|
||||||
RSpec/NestedGroups:
|
RSpec/NestedGroups:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Max: 4
|
Max: 4
|
||||||
|
|
||||||
RSpec/MessageSpies:
|
RSpec/MessageSpies:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
RSpec/StubbedMock:
|
RSpec/StubbedMock:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
RSpec/FactoryBot/SyntaxMethods:
|
|
||||||
Enabled: false
|
|
||||||
Naming/VariableNumber:
|
Naming/VariableNumber:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Naming/MemoizedInstanceVariableName:
|
Naming/MemoizedInstanceVariableName:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/message.rb'
|
- 'app/models/message.rb'
|
||||||
|
|
||||||
Style/GuardClause:
|
Style/GuardClause:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/builders/account_builder.rb'
|
- 'app/builders/account_builder.rb'
|
||||||
- 'app/models/attachment.rb'
|
- 'app/models/attachment.rb'
|
||||||
- 'app/models/message.rb'
|
- 'app/models/message.rb'
|
||||||
|
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 26
|
Max: 26
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/concerns/auth_helper.rb'
|
- '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/integrations/hook.rb'
|
||||||
- 'app/models/canned_response.rb'
|
- 'app/models/canned_response.rb'
|
||||||
- 'app/models/telegram_bot.rb'
|
- 'app/models/telegram_bot.rb'
|
||||||
|
|
||||||
Rails/RenderInline:
|
Rails/RenderInline:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/swagger_controller.rb'
|
- 'app/controllers/swagger_controller.rb'
|
||||||
|
|
||||||
Rails/ThreeStateBooleanColumn:
|
Rails/ThreeStateBooleanColumn:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/migrate/20230503101201_create_sla_policies.rb'
|
- 'db/migrate/20230503101201_create_sla_policies.rb'
|
||||||
|
|
||||||
RSpec/IndexedLet:
|
RSpec/IndexedLet:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
RSpec/NamedSubject:
|
RSpec/NamedSubject:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
# we should bring this down
|
# we should bring this down
|
||||||
RSpec/MultipleExpectations:
|
RSpec/MultipleExpectations:
|
||||||
Max: 7
|
Max: 7
|
||||||
|
|
||||||
RSpec/MultipleMemoizedHelpers:
|
RSpec/MultipleMemoizedHelpers:
|
||||||
Max: 14
|
Max: 14
|
||||||
|
|
||||||
@@ -166,3 +219,121 @@ AllCops:
|
|||||||
- 'tmp/**/*'
|
- 'tmp/**/*'
|
||||||
- 'storage/**/*'
|
- 'storage/**/*'
|
||||||
- 'db/migrate/20230426130150_init_schema.rb'
|
- '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'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '3.3.3'
|
ruby '3.4.4'
|
||||||
|
|
||||||
##-- base gems for rails --##
|
##-- base gems for rails --##
|
||||||
gem 'rack-cors', '2.0.0', require: 'rack/cors'
|
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
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
|
|
||||||
@@ -33,6 +33,8 @@ gem 'liquid'
|
|||||||
gem 'commonmarker'
|
gem 'commonmarker'
|
||||||
# Validate Data against JSON Schema
|
# Validate Data against JSON Schema
|
||||||
gem 'json_schemer'
|
gem 'json_schemer'
|
||||||
|
# used in swagger build
|
||||||
|
gem 'json_refs'
|
||||||
# Rack middleware for blocking & throttling abusive requests
|
# Rack middleware for blocking & throttling abusive requests
|
||||||
gem 'rack-attack', '>= 6.7.0'
|
gem 'rack-attack', '>= 6.7.0'
|
||||||
# a utility tool for streaming, flexible and safe downloading of remote files
|
# 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 'scss_lint', require: false
|
||||||
gem 'web-console', '>= 4.2.1'
|
gem 'web-console', '>= 4.2.1'
|
||||||
|
|
||||||
# used in swagger build
|
|
||||||
gem 'json_refs'
|
|
||||||
|
|
||||||
# When we want to squash migrations
|
# When we want to squash migrations
|
||||||
gem 'squasher'
|
gem 'squasher'
|
||||||
|
|
||||||
@@ -237,6 +236,7 @@ group :development, :test do
|
|||||||
gem 'rubocop-performance', require: false
|
gem 'rubocop-performance', require: false
|
||||||
gem 'rubocop-rails', require: false
|
gem 'rubocop-rails', require: false
|
||||||
gem 'rubocop-rspec', require: false
|
gem 'rubocop-rspec', require: false
|
||||||
|
gem 'rubocop-factory_bot', require: false
|
||||||
gem 'seed_dump'
|
gem 'seed_dump'
|
||||||
gem 'shoulda-matchers'
|
gem 'shoulda-matchers'
|
||||||
gem 'simplecov', '0.17.1', require: false
|
gem 'simplecov', '0.17.1', require: false
|
||||||
|
|||||||
+160
-140
@@ -25,76 +25,89 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (7.0.8.7)
|
actioncable (7.1.5.1)
|
||||||
actionpack (= 7.0.8.7)
|
actionpack (= 7.1.5.1)
|
||||||
activesupport (= 7.0.8.7)
|
activesupport (= 7.1.5.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (7.0.8.7)
|
zeitwerk (~> 2.6)
|
||||||
actionpack (= 7.0.8.7)
|
actionmailbox (7.1.5.1)
|
||||||
activejob (= 7.0.8.7)
|
actionpack (= 7.1.5.1)
|
||||||
activerecord (= 7.0.8.7)
|
activejob (= 7.1.5.1)
|
||||||
activestorage (= 7.0.8.7)
|
activerecord (= 7.1.5.1)
|
||||||
activesupport (= 7.0.8.7)
|
activestorage (= 7.1.5.1)
|
||||||
|
activesupport (= 7.1.5.1)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
actionmailer (7.0.8.7)
|
actionmailer (7.1.5.1)
|
||||||
actionpack (= 7.0.8.7)
|
actionpack (= 7.1.5.1)
|
||||||
actionview (= 7.0.8.7)
|
actionview (= 7.1.5.1)
|
||||||
activejob (= 7.0.8.7)
|
activejob (= 7.1.5.1)
|
||||||
activesupport (= 7.0.8.7)
|
activesupport (= 7.1.5.1)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (7.0.8.7)
|
actionpack (7.1.5.1)
|
||||||
actionview (= 7.0.8.7)
|
actionview (= 7.1.5.1)
|
||||||
activesupport (= 7.0.8.7)
|
activesupport (= 7.1.5.1)
|
||||||
rack (~> 2.0, >= 2.2.4)
|
nokogiri (>= 1.8.5)
|
||||||
|
racc
|
||||||
|
rack (>= 2.2.4)
|
||||||
|
rack-session (>= 1.0.1)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actiontext (7.0.8.7)
|
actiontext (7.1.5.1)
|
||||||
actionpack (= 7.0.8.7)
|
actionpack (= 7.1.5.1)
|
||||||
activerecord (= 7.0.8.7)
|
activerecord (= 7.1.5.1)
|
||||||
activestorage (= 7.0.8.7)
|
activestorage (= 7.1.5.1)
|
||||||
activesupport (= 7.0.8.7)
|
activesupport (= 7.1.5.1)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.0.8.7)
|
actionview (7.1.5.1)
|
||||||
activesupport (= 7.0.8.7)
|
activesupport (= 7.1.5.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.6)
|
||||||
active_record_query_trace (1.8)
|
active_record_query_trace (1.8)
|
||||||
activejob (7.0.8.7)
|
activejob (7.1.5.1)
|
||||||
activesupport (= 7.0.8.7)
|
activesupport (= 7.1.5.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.0.8.7)
|
activemodel (7.1.5.1)
|
||||||
activesupport (= 7.0.8.7)
|
activesupport (= 7.1.5.1)
|
||||||
activerecord (7.0.8.7)
|
activerecord (7.1.5.1)
|
||||||
activemodel (= 7.0.8.7)
|
activemodel (= 7.1.5.1)
|
||||||
activesupport (= 7.0.8.7)
|
activesupport (= 7.1.5.1)
|
||||||
activerecord-import (1.4.1)
|
timeout (>= 0.4.0)
|
||||||
|
activerecord-import (2.1.0)
|
||||||
activerecord (>= 4.2)
|
activerecord (>= 4.2)
|
||||||
activestorage (7.0.8.7)
|
activestorage (7.1.5.1)
|
||||||
actionpack (= 7.0.8.7)
|
actionpack (= 7.1.5.1)
|
||||||
activejob (= 7.0.8.7)
|
activejob (= 7.1.5.1)
|
||||||
activerecord (= 7.0.8.7)
|
activerecord (= 7.1.5.1)
|
||||||
activesupport (= 7.0.8.7)
|
activesupport (= 7.1.5.1)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
activesupport (7.1.5.1)
|
||||||
activesupport (7.0.8.7)
|
base64
|
||||||
|
benchmark (>= 0.3)
|
||||||
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
|
connection_pool (>= 2.2.5)
|
||||||
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
|
logger (>= 1.4.2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
|
mutex_m
|
||||||
|
securerandom (>= 0.3)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
acts-as-taggable-on (9.0.1)
|
acts-as-taggable-on (12.0.0)
|
||||||
activerecord (>= 6.0, < 7.1)
|
activerecord (>= 7.1, < 8.1)
|
||||||
|
zeitwerk (>= 2.4, < 3.0)
|
||||||
addressable (2.8.7)
|
addressable (2.8.7)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
administrate (0.20.1)
|
administrate (0.20.1)
|
||||||
@@ -116,7 +129,7 @@ GEM
|
|||||||
annotate (3.2.0)
|
annotate (3.2.0)
|
||||||
activerecord (>= 3.2, < 8.0)
|
activerecord (>= 3.2, < 8.0)
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
ast (2.4.2)
|
ast (2.4.3)
|
||||||
attr_extras (7.1.0)
|
attr_extras (7.1.0)
|
||||||
audited (5.4.1)
|
audited (5.4.1)
|
||||||
activerecord (>= 5.0, < 7.7)
|
activerecord (>= 5.0, < 7.7)
|
||||||
@@ -142,14 +155,15 @@ GEM
|
|||||||
statsd-ruby (~> 1.1)
|
statsd-ruby (~> 1.1)
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
bigdecimal (3.1.8)
|
benchmark (0.4.0)
|
||||||
|
bigdecimal (3.1.9)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.16.0)
|
bootsnap (1.16.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (5.4.1)
|
brakeman (5.4.1)
|
||||||
browser (5.3.1)
|
browser (5.3.1)
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
bullet (7.0.7)
|
bullet (8.0.7)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
bundle-audit (0.1.0)
|
bundle-audit (0.1.0)
|
||||||
@@ -161,8 +175,8 @@ GEM
|
|||||||
climate_control (1.2.0)
|
climate_control (1.2.0)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
commonmarker (0.23.10)
|
commonmarker (0.23.10)
|
||||||
concurrent-ruby (1.3.4)
|
concurrent-ruby (1.3.5)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.5.3)
|
||||||
crack (1.0.0)
|
crack (1.0.0)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
@@ -176,16 +190,10 @@ GEM
|
|||||||
activerecord (>= 5.a)
|
activerecord (>= 5.a)
|
||||||
database_cleaner-core (~> 2.0.0)
|
database_cleaner-core (~> 2.0.0)
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
datadog-ci (0.8.3)
|
|
||||||
msgpack
|
|
||||||
date (3.4.1)
|
date (3.4.1)
|
||||||
ddtrace (1.23.2)
|
ddtrace (0.48.0)
|
||||||
datadog-ci (~> 0.8.1)
|
ffi (~> 1.0)
|
||||||
debase-ruby_core_source (= 3.3.1)
|
|
||||||
libdatadog (~> 7.0.0.1.0)
|
|
||||||
libddwaf (~> 1.14.0.0.0)
|
|
||||||
msgpack
|
msgpack
|
||||||
debase-ruby_core_source (3.3.1)
|
|
||||||
debug (1.8.0)
|
debug (1.8.0)
|
||||||
irb (>= 1.5.0)
|
irb (>= 1.5.0)
|
||||||
reline (>= 0.3.1)
|
reline (>= 0.3.1)
|
||||||
@@ -196,10 +204,10 @@ GEM
|
|||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise_token_auth (1.2.3)
|
devise_token_auth (1.2.5)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
devise (> 3.5.2, < 5)
|
devise (> 3.5.2, < 5)
|
||||||
rails (>= 4.2.0, < 7.2)
|
rails (>= 4.2.0, < 8.1)
|
||||||
diff-lcs (1.5.1)
|
diff-lcs (1.5.1)
|
||||||
digest-crc (0.6.5)
|
digest-crc (0.6.5)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
@@ -212,6 +220,7 @@ GEM
|
|||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
down (5.4.0)
|
down (5.4.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
|
drb (2.2.3)
|
||||||
dry-cli (1.1.0)
|
dry-cli (1.1.0)
|
||||||
ecma-re-validator (0.4.0)
|
ecma-re-validator (0.4.0)
|
||||||
regexp_parser (~> 2.2)
|
regexp_parser (~> 2.2)
|
||||||
@@ -254,7 +263,10 @@ GEM
|
|||||||
fcm (1.0.8)
|
fcm (1.0.8)
|
||||||
faraday (>= 1.0.0, < 3.0)
|
faraday (>= 1.0.0, < 3.0)
|
||||||
googleauth (~> 1)
|
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-compiler (1.0.1)
|
||||||
ffi (>= 1.0.0)
|
ffi (>= 1.0.0)
|
||||||
rake
|
rake
|
||||||
@@ -315,16 +327,13 @@ GEM
|
|||||||
google-cloud-translate-v3 (0.10.0)
|
google-cloud-translate-v3 (0.10.0)
|
||||||
gapic-common (>= 0.20.0, < 2.a)
|
gapic-common (>= 0.20.0, < 2.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-protobuf (3.25.5)
|
google-protobuf (3.25.7)
|
||||||
google-protobuf (3.25.5-arm64-darwin)
|
|
||||||
google-protobuf (3.25.5-x86_64-darwin)
|
|
||||||
google-protobuf (3.25.5-x86_64-linux)
|
|
||||||
googleapis-common-protos (1.6.0)
|
googleapis-common-protos (1.6.0)
|
||||||
google-protobuf (>= 3.18, < 5.a)
|
google-protobuf (>= 3.18, < 5.a)
|
||||||
googleapis-common-protos-types (~> 1.7)
|
googleapis-common-protos-types (~> 1.7)
|
||||||
grpc (~> 1.41)
|
grpc (~> 1.41)
|
||||||
googleapis-common-protos-types (1.14.0)
|
googleapis-common-protos-types (1.20.0)
|
||||||
google-protobuf (~> 3.18)
|
google-protobuf (>= 3.18, < 5.a)
|
||||||
googleauth (1.11.2)
|
googleauth (1.11.2)
|
||||||
faraday (>= 1.0, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-env (~> 2.1)
|
google-cloud-env (~> 2.1)
|
||||||
@@ -334,17 +343,17 @@ GEM
|
|||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
groupdate (6.2.1)
|
groupdate (6.2.1)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
grpc (1.62.0)
|
grpc (1.72.0)
|
||||||
google-protobuf (~> 3.25)
|
google-protobuf (>= 3.25, < 5.0)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
grpc (1.62.0-arm64-darwin)
|
grpc (1.72.0-arm64-darwin)
|
||||||
google-protobuf (~> 3.25)
|
google-protobuf (>= 3.25, < 5.0)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
grpc (1.62.0-x86_64-darwin)
|
grpc (1.72.0-x86_64-darwin)
|
||||||
google-protobuf (~> 3.25)
|
google-protobuf (>= 3.25, < 5.0)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
grpc (1.62.0-x86_64-linux)
|
grpc (1.72.0-x86_64-linux)
|
||||||
google-protobuf (~> 3.25)
|
google-protobuf (>= 3.25, < 5.0)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
haikunator (1.1.1)
|
haikunator (1.1.1)
|
||||||
hairtrigger (1.0.0)
|
hairtrigger (1.0.0)
|
||||||
@@ -370,7 +379,7 @@ GEM
|
|||||||
mini_mime (>= 1.0.0)
|
mini_mime (>= 1.0.0)
|
||||||
multi_xml (>= 0.5.2)
|
multi_xml (>= 0.5.2)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.14.6)
|
i18n (1.14.7)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
image_processing (1.12.2)
|
image_processing (1.12.2)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
@@ -388,7 +397,7 @@ GEM
|
|||||||
rails-dom-testing (>= 1, < 3)
|
rails-dom-testing (>= 1, < 3)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
thor (>= 0.14, < 2.0)
|
thor (>= 0.14, < 2.0)
|
||||||
json (2.6.3)
|
json (2.12.0)
|
||||||
json_refs (0.1.8)
|
json_refs (0.1.8)
|
||||||
hana
|
hana
|
||||||
json_schemer (0.2.24)
|
json_schemer (0.2.24)
|
||||||
@@ -423,21 +432,13 @@ GEM
|
|||||||
faraday-multipart
|
faraday-multipart
|
||||||
json (>= 1.8)
|
json (>= 1.8)
|
||||||
rexml
|
rexml
|
||||||
|
language_server-protocol (3.17.0.5)
|
||||||
launchy (2.5.2)
|
launchy (2.5.2)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
letter_opener (1.8.1)
|
letter_opener (1.8.1)
|
||||||
launchy (>= 2.2, < 3)
|
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)
|
line-bot-api (1.28.0)
|
||||||
|
lint_roller (1.1.0)
|
||||||
liquid (5.4.0)
|
liquid (5.4.0)
|
||||||
listen (3.8.0)
|
listen (3.8.0)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
@@ -445,7 +446,7 @@ GEM
|
|||||||
llhttp-ffi (0.4.0)
|
llhttp-ffi (0.4.0)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
logger (1.6.0)
|
logger (1.7.0)
|
||||||
lograge (0.14.0)
|
lograge (0.14.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
@@ -471,10 +472,10 @@ GEM
|
|||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.8)
|
mini_portile2 (2.8.8)
|
||||||
minitest (5.25.4)
|
minitest (5.25.5)
|
||||||
mock_redis (0.36.0)
|
mock_redis (0.36.0)
|
||||||
ruby2_keywords
|
ruby2_keywords
|
||||||
msgpack (1.7.0)
|
msgpack (1.8.0)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multi_xml (0.6.0)
|
multi_xml (0.6.0)
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
@@ -545,14 +546,16 @@ GEM
|
|||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
ostruct (0.6.1)
|
ostruct (0.6.1)
|
||||||
parallel (1.23.0)
|
parallel (1.27.0)
|
||||||
parser (3.2.2.1)
|
parser (3.3.8.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
|
racc
|
||||||
pg (1.5.3)
|
pg (1.5.3)
|
||||||
pg_search (2.3.6)
|
pg_search (2.3.6)
|
||||||
activerecord (>= 5.2)
|
activerecord (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
pgvector (0.1.1)
|
pgvector (0.1.1)
|
||||||
|
prism (1.4.0)
|
||||||
procore-sift (1.0.0)
|
procore-sift (1.0.0)
|
||||||
activerecord (>= 6.1)
|
activerecord (>= 6.1)
|
||||||
pry (0.14.2)
|
pry (0.14.2)
|
||||||
@@ -567,7 +570,7 @@ GEM
|
|||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (2.2.14)
|
rack (2.2.15)
|
||||||
rack-attack (6.7.0)
|
rack-attack (6.7.0)
|
||||||
rack (>= 1.0, < 4)
|
rack (>= 1.0, < 4)
|
||||||
rack-contrib (2.5.0)
|
rack-contrib (2.5.0)
|
||||||
@@ -581,23 +584,28 @@ GEM
|
|||||||
rack (~> 2.2, >= 2.2.4)
|
rack (~> 2.2, >= 2.2.4)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
|
rack-session (1.0.2)
|
||||||
|
rack (< 3)
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rack-timeout (0.6.3)
|
rack-timeout (0.6.3)
|
||||||
rails (7.0.8.7)
|
rackup (1.0.1)
|
||||||
actioncable (= 7.0.8.7)
|
rack (< 3)
|
||||||
actionmailbox (= 7.0.8.7)
|
webrick
|
||||||
actionmailer (= 7.0.8.7)
|
rails (7.1.5.1)
|
||||||
actionpack (= 7.0.8.7)
|
actioncable (= 7.1.5.1)
|
||||||
actiontext (= 7.0.8.7)
|
actionmailbox (= 7.1.5.1)
|
||||||
actionview (= 7.0.8.7)
|
actionmailer (= 7.1.5.1)
|
||||||
activejob (= 7.0.8.7)
|
actionpack (= 7.1.5.1)
|
||||||
activemodel (= 7.0.8.7)
|
actiontext (= 7.1.5.1)
|
||||||
activerecord (= 7.0.8.7)
|
actionview (= 7.1.5.1)
|
||||||
activestorage (= 7.0.8.7)
|
activejob (= 7.1.5.1)
|
||||||
activesupport (= 7.0.8.7)
|
activemodel (= 7.1.5.1)
|
||||||
|
activerecord (= 7.1.5.1)
|
||||||
|
activestorage (= 7.1.5.1)
|
||||||
|
activesupport (= 7.1.5.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.0.8.7)
|
railties (= 7.1.5.1)
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
@@ -605,13 +613,14 @@ GEM
|
|||||||
rails-html-sanitizer (1.6.1)
|
rails-html-sanitizer (1.6.1)
|
||||||
loofah (~> 2.21)
|
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)
|
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)
|
railties (7.1.5.1)
|
||||||
actionpack (= 7.0.8.7)
|
actionpack (= 7.1.5.1)
|
||||||
activesupport (= 7.0.8.7)
|
activesupport (= 7.1.5.1)
|
||||||
method_source
|
irb
|
||||||
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.2.1)
|
rake (13.2.1)
|
||||||
rb-fsevent (0.11.2)
|
rb-fsevent (0.11.2)
|
||||||
@@ -623,7 +632,7 @@ GEM
|
|||||||
connection_pool
|
connection_pool
|
||||||
redis-namespace (1.10.0)
|
redis-namespace (1.10.0)
|
||||||
redis (>= 4)
|
redis (>= 4)
|
||||||
regexp_parser (2.8.0)
|
regexp_parser (2.10.0)
|
||||||
reline (0.3.6)
|
reline (0.3.6)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
@@ -643,7 +652,7 @@ GEM
|
|||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
reverse_markdown (2.1.1)
|
reverse_markdown (2.1.1)
|
||||||
nokogiri
|
nokogiri
|
||||||
rexml (3.3.9)
|
rexml (3.4.1)
|
||||||
rspec-core (3.13.0)
|
rspec-core (3.13.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.13.2)
|
rspec-expectations (3.13.2)
|
||||||
@@ -663,30 +672,36 @@ GEM
|
|||||||
rspec-support (3.13.1)
|
rspec-support (3.13.1)
|
||||||
rspec_junit_formatter (0.6.0)
|
rspec_junit_formatter (0.6.0)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (1.50.2)
|
rubocop (1.75.6)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
|
language_server-protocol (~> 3.17.0.2)
|
||||||
|
lint_roller (~> 1.1.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.2.0.0)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rubocop-ast (>= 1.44.0, < 2.0)
|
||||||
rubocop-ast (>= 1.28.0, < 2.0)
|
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.28.1)
|
rubocop-ast (1.44.1)
|
||||||
parser (>= 3.2.1.0)
|
parser (>= 3.3.7.2)
|
||||||
rubocop-capybara (2.18.0)
|
prism (~> 1.4)
|
||||||
rubocop (~> 1.41)
|
rubocop-factory_bot (2.27.1)
|
||||||
rubocop-performance (1.17.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
rubocop-ast (>= 0.4.0)
|
rubocop-performance (1.25.0)
|
||||||
rubocop-rails (2.19.1)
|
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)
|
activesupport (>= 4.2.0)
|
||||||
|
lint_roller (~> 1.1)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
rubocop-rspec (2.21.0)
|
rubocop-ast (>= 1.44.0, < 2.0)
|
||||||
rubocop (~> 1.33)
|
rubocop-rspec (3.6.0)
|
||||||
rubocop-capybara (~> 2.17)
|
lint_roller (~> 1.1)
|
||||||
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
ruby-openai (7.3.1)
|
ruby-openai (7.3.1)
|
||||||
event_stream_parser (>= 0.3.0, < 2.0.0)
|
event_stream_parser (>= 0.3.0, < 2.0.0)
|
||||||
faraday (>= 1)
|
faraday (>= 1)
|
||||||
@@ -816,8 +831,10 @@ GEM
|
|||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.8.2)
|
unf_ext (0.0.8.2)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (3.1.4)
|
||||||
uniform_notifier (1.16.0)
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||||
|
unicode-emoji (4.0.4)
|
||||||
|
uniform_notifier (1.17.0)
|
||||||
uri (1.0.3)
|
uri (1.0.3)
|
||||||
uri_template (0.7.0)
|
uri_template (0.7.0)
|
||||||
valid_email2 (5.2.6)
|
valid_email2 (5.2.6)
|
||||||
@@ -845,7 +862,9 @@ GEM
|
|||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
websocket-driver (0.7.6)
|
webrick (1.9.1)
|
||||||
|
websocket-driver (0.7.7)
|
||||||
|
base64
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
wisper (2.0.0)
|
wisper (2.0.0)
|
||||||
@@ -951,7 +970,7 @@ DEPENDENCIES
|
|||||||
rack-cors (= 2.0.0)
|
rack-cors (= 2.0.0)
|
||||||
rack-mini-profiler (>= 3.2.0)
|
rack-mini-profiler (>= 3.2.0)
|
||||||
rack-timeout
|
rack-timeout
|
||||||
rails (~> 7.0.8.4)
|
rails (~> 7.1)
|
||||||
redis
|
redis
|
||||||
redis-namespace
|
redis-namespace
|
||||||
responders (>= 3.1.1)
|
responders (>= 3.1.1)
|
||||||
@@ -960,6 +979,7 @@ DEPENDENCIES
|
|||||||
rspec-rails (>= 6.1.5)
|
rspec-rails (>= 6.1.5)
|
||||||
rspec_junit_formatter
|
rspec_junit_formatter
|
||||||
rubocop
|
rubocop
|
||||||
|
rubocop-factory_bot
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
rubocop-rspec
|
rubocop-rspec
|
||||||
@@ -997,7 +1017,7 @@ DEPENDENCIES
|
|||||||
working_hours
|
working_hours
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.3.3p89
|
ruby 3.4.4p34
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.16
|
2.5.16
|
||||||
|
|||||||
@@ -32,14 +32,7 @@ class AccountBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_email
|
def validate_email
|
||||||
raise InvalidEmail.new({ domain_blocked: domain_blocked }) if domain_blocked?
|
Account::SignUpEmailValidationService.new(@email).perform
|
||||||
|
|
||||||
address = ValidEmail2::Address.new(@email)
|
|
||||||
if address.valid? && !address.disposable?
|
|
||||||
true
|
|
||||||
else
|
|
||||||
raise InvalidEmail.new({ valid: address.valid?, disposable: address.disposable? })
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_user
|
def validate_user
|
||||||
@@ -81,21 +74,4 @@ class AccountBuilder
|
|||||||
@user.confirm if @confirmed
|
@user.confirm if @confirmed
|
||||||
@user.save!
|
@user.save!
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -59,11 +59,13 @@ class ContactInboxBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_contact_inbox
|
def create_contact_inbox
|
||||||
::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
|
attrs = {
|
||||||
contact_id: @contact.id,
|
contact_id: @contact.id,
|
||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
source_id: @source_id
|
source_id: @source_id
|
||||||
)
|
}
|
||||||
|
|
||||||
|
::ContactInbox.where(attrs).first_or_create!(hmac_verified: hmac_verified || false)
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
Rails.logger.info("[ContactInboxBuilder] RecordNotUnique #{@source_id} #{@contact.id} #{@inbox.id}")
|
Rails.logger.info("[ContactInboxBuilder] RecordNotUnique #{@source_id} #{@contact.id} #{@inbox.id}")
|
||||||
update_old_contact_inbox
|
update_old_contact_inbox
|
||||||
|
|||||||
@@ -152,11 +152,13 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
|
|||||||
end
|
end
|
||||||
|
|
||||||
def message_already_exists?
|
def message_already_exists?
|
||||||
cw_message = conversation.messages.where(
|
find_message_by_source_id(@messaging[:message][:mid]).present?
|
||||||
source_id: @messaging[:message][:mid]
|
end
|
||||||
).first
|
|
||||||
|
|
||||||
cw_message.present?
|
def find_message_by_source_id(source_id)
|
||||||
|
return unless source_id
|
||||||
|
|
||||||
|
@message = Message.find_by(source_id: source_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def all_unsupported_files?
|
def all_unsupported_files?
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
|||||||
|
|
||||||
def article_params
|
def article_params
|
||||||
params.require(:article).permit(
|
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,
|
:locale, meta: [:title,
|
||||||
:description,
|
:description,
|
||||||
{ tags: [] }]
|
{ tags: [] }]
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
|
|||||||
Current.account
|
Current.account
|
||||||
).perform
|
).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)
|
@conversations = conversations.order(last_activity_at: :desc).limit(20)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
|
|||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
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
|
end
|
||||||
|
|
||||||
def fetch_hook
|
def fetch_hook
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
|
|||||||
|
|
||||||
def sign_up_user
|
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: '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
|
create_account_for_user
|
||||||
token = @resource.send(:set_reset_password_token)
|
token = @resource.send(:set_reset_password_token)
|
||||||
@@ -53,9 +53,11 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
|
|||||||
).first
|
).first
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_business_account?
|
def validate_signup_email_is_business_domain?
|
||||||
# return true if the user is a business account, false if it is a gmail account
|
# return true if the user is a business account, false if it is a blocked domain account
|
||||||
auth_hash['info']['email'].downcase.exclude?('@gmail.com')
|
Account::SignUpEmailValidationService.new(auth_hash['info']['email']).perform
|
||||||
|
rescue CustomExceptions::Account::InvalidEmail
|
||||||
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_account_for_user
|
def create_account_for_user
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
class Platform::Api::V1::UsersController < PlatformController
|
class Platform::Api::V1::UsersController < PlatformController
|
||||||
# ref: https://stackoverflow.com/a/45190318/939299
|
# ref: https://stackoverflow.com/a/45190318/939299
|
||||||
# set resource is called for other actions already in platform controller
|
# set resource is called for other actions already in platform controller
|
||||||
# we want to add login to that chain as well
|
# we want to add login and token to that chain as well
|
||||||
before_action(only: [:login]) { set_resource }
|
before_action(only: [:login, :token]) { set_resource }
|
||||||
before_action(only: [:login]) { validate_platform_app_permissible }
|
before_action(only: [:login, :token]) { validate_platform_app_permissible }
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
@@ -18,6 +18,8 @@ class Platform::Api::V1::UsersController < PlatformController
|
|||||||
render json: { url: @resource.generate_sso_link }
|
render json: { url: @resource.generate_sso_link }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def token; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@resource.assign_attributes(user_update_params)
|
@resource.assign_attributes(user_update_params)
|
||||||
|
|
||||||
|
|||||||
@@ -32,22 +32,17 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def allowed_configs
|
def allowed_configs
|
||||||
@allowed_configs = case @config
|
mapping = {
|
||||||
when 'facebook'
|
'facebook' => %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT],
|
||||||
%w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT]
|
'shopify' => %w[SHOPIFY_CLIENT_ID SHOPIFY_CLIENT_SECRET],
|
||||||
when 'shopify'
|
'microsoft' => %w[AZURE_APP_ID AZURE_APP_SECRET],
|
||||||
%w[SHOPIFY_CLIENT_ID SHOPIFY_CLIENT_SECRET]
|
'email' => ['MAILER_INBOUND_EMAIL_DOMAIN'],
|
||||||
when 'microsoft'
|
'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET],
|
||||||
%w[AZURE_APP_ID AZURE_APP_SECRET]
|
'slack' => %w[SLACK_CLIENT_ID SLACK_CLIENT_SECRET],
|
||||||
when 'email'
|
'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT]
|
||||||
['MAILER_INBOUND_EMAIL_DOMAIN']
|
}
|
||||||
when 'linear'
|
|
||||||
%w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET]
|
@allowed_configs = mapping.fetch(@config, %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS])
|
||||||
when 'instagram'
|
|
||||||
%w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT]
|
|
||||||
else
|
|
||||||
%w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,10 @@ class ConversationFinder
|
|||||||
|
|
||||||
def find_conversation_by_inbox
|
def find_conversation_by_inbox
|
||||||
@conversations = current_account.conversations
|
@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
|
end
|
||||||
|
|
||||||
def find_all_conversations
|
def find_all_conversations
|
||||||
|
|||||||
+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 Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||||
import Flag from 'dashboard/components-next/flag/Flag.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';
|
import countries from 'shared/constants/countries';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -149,15 +150,15 @@ const onClickViewDetails = () => emit('showContact', props.id);
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<template #after>
|
<template #after>
|
||||||
<transition
|
<div
|
||||||
enter-active-class="overflow-hidden transition-all duration-300 ease-out"
|
class="transition-all duration-500 ease-in-out grid overflow-hidden"
|
||||||
leave-active-class="overflow-hidden transition-all duration-300 ease-in"
|
:class="
|
||||||
enter-from-class="overflow-hidden opacity-0 max-h-0"
|
isExpanded
|
||||||
enter-to-class="opacity-100 max-h-[690px] sm:max-h-[470px] md:max-h-[410px]"
|
? 'grid-rows-[1fr] opacity-100'
|
||||||
leave-from-class="opacity-100 max-h-[690px] sm:max-h-[470px] md:max-h-[410px]"
|
: 'grid-rows-[0fr] opacity-0'
|
||||||
leave-to-class="overflow-hidden opacity-0 max-h-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">
|
<div class="flex flex-col gap-6 p-6 border-t border-n-strong">
|
||||||
<ContactsForm
|
<ContactsForm
|
||||||
ref="contactsFormRef"
|
ref="contactsFormRef"
|
||||||
@@ -176,8 +177,14 @@ const onClickViewDetails = () => emit('showContact', props.id);
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ContactDeleteSection
|
||||||
|
:selected-contact="{
|
||||||
|
id: props.id,
|
||||||
|
name: props.name,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</CardLayout>
|
</CardLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+1
-5
@@ -47,11 +47,7 @@ defineExpose({ dialogRef });
|
|||||||
ref="dialogRef"
|
ref="dialogRef"
|
||||||
type="alert"
|
type="alert"
|
||||||
:title="t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.TITLE')"
|
:title="t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.TITLE')"
|
||||||
:description="
|
:description="t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.DESCRIPTION')"
|
||||||
t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.DESCRIPTION', {
|
|
||||||
contactName: props.selectedContact.name,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
:confirm-button-label="t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.CONFIRM')"
|
:confirm-button-label="t('CONTACTS_LAYOUT.DETAILS.DELETE_DIALOG.CONFIRM')"
|
||||||
@confirm="handleDialogConfirm"
|
@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',
|
'text-n-ruby-11 hover:enabled:bg-n-ruby-9/10 focus-visible:bg-n-ruby-9/10 outline-n-ruby-8',
|
||||||
ghost:
|
ghost:
|
||||||
'text-n-ruby-11 hover:enabled:bg-n-alpha-2 focus-visible:bg-n-alpha-2 outline-transparent',
|
'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: {
|
amber: {
|
||||||
solid:
|
solid:
|
||||||
|
|||||||
+23
-1
@@ -42,6 +42,7 @@ const initialState = {
|
|||||||
conversationFaqs: false,
|
conversationFaqs: false,
|
||||||
memories: false,
|
memories: false,
|
||||||
},
|
},
|
||||||
|
temperature: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const state = reactive({ ...initialState });
|
const state = reactive({ ...initialState });
|
||||||
@@ -87,6 +88,7 @@ const updateStateFromAssistant = assistant => {
|
|||||||
conversationFaqs: config.feature_faq || false,
|
conversationFaqs: config.feature_faq || false,
|
||||||
memories: config.feature_memory || false,
|
memories: config.feature_memory || false,
|
||||||
};
|
};
|
||||||
|
state.temperature = config.temperature || 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBasicInfoUpdate = async () => {
|
const handleBasicInfoUpdate = async () => {
|
||||||
@@ -136,6 +138,7 @@ const handleInstructionsUpdate = async () => {
|
|||||||
const payload = {
|
const payload = {
|
||||||
config: {
|
config: {
|
||||||
...props.assistant.config,
|
...props.assistant.config,
|
||||||
|
temperature: state.temperature || 1,
|
||||||
instructions: state.instructions,
|
instructions: state.instructions,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -212,7 +215,7 @@ watch(
|
|||||||
|
|
||||||
<!-- Instructions Section -->
|
<!-- Instructions Section -->
|
||||||
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.INSTRUCTIONS')">
|
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.INSTRUCTIONS')">
|
||||||
<div class="flex flex-col gap-4 pt-4">
|
<div class="flex flex-col gap-4">
|
||||||
<Editor
|
<Editor
|
||||||
v-model="state.instructions"
|
v-model="state.instructions"
|
||||||
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
|
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
|
||||||
@@ -221,6 +224,25 @@ watch(
|
|||||||
:message-type="formErrors.instructions ? 'error' : 'info'"
|
:message-type="formErrors.instructions ? 'error' : 'info'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 mt-4">
|
||||||
|
<label class="text-sm font-medium text-n-slate-12">
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.LABEL') }}
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<input
|
||||||
|
v-model="state.temperature"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-n-slate-12">{{ state.temperature }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-n-slate-11 italic">
|
||||||
|
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.DESCRIPTION') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -1,31 +1,17 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Avatar from '../avatar/Avatar.vue';
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
message: {
|
message: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
supportAgent: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="space-y-1 text-n-slate-12">
|
||||||
<Avatar
|
<div class="font-medium">{{ $t('CAPTAIN.COPILOT.YOU') }}</div>
|
||||||
:name="supportAgent.available_name"
|
<div class="break-words">
|
||||||
:src="supportAgent.avatar_url"
|
{{ message.content }}
|
||||||
: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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
|||||||
import MessageFormatter from 'shared/helpers/MessageFormatter.js';
|
import MessageFormatter from 'shared/helpers/MessageFormatter.js';
|
||||||
|
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import Avatar from '../avatar/Avatar.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
message: {
|
message: {
|
||||||
@@ -46,33 +45,25 @@ const useCopilotResponse = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-col gap-1 text-n-slate-12">
|
||||||
<Avatar
|
<div class="font-medium">{{ $t('CAPTAIN.NAME') }}</div>
|
||||||
name="Captain Copilot"
|
<span v-if="hasEmptyMessageContent" class="text-n-ruby-11">
|
||||||
icon-name="i-woot-captain"
|
{{ $t('CAPTAIN.COPILOT.EMPTY_MESSAGE') }}
|
||||||
:size="24"
|
</span>
|
||||||
rounded-full
|
<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="flex flex-row mt-1">
|
||||||
<div class="font-medium">{{ $t('CAPTAIN.NAME') }}</div>
|
<Button
|
||||||
<span v-if="hasEmptyMessageContent" class="text-n-ruby-11">
|
v-if="!hasEmptyMessageContent"
|
||||||
{{ $t('CAPTAIN.COPILOT.EMPTY_MESSAGE') }}
|
:label="$t('CAPTAIN.COPILOT.USE')"
|
||||||
</span>
|
faded
|
||||||
<div
|
sm
|
||||||
v-else
|
slate
|
||||||
v-dompurify-html="messageContent"
|
@click="useCopilotResponse"
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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-5 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form
|
<form class="relative" @submit.prevent="sendMessage">
|
||||||
class="border border-n-weak bg-n-alpha-3 rounded-lg h-12 flex"
|
|
||||||
@submit.prevent="sendMessage"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
v-model="message"
|
v-model="message"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('CAPTAIN.COPILOT.SEND_MESSAGE')"
|
: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"
|
@keyup.enter="sendMessage"
|
||||||
/>
|
/>
|
||||||
<button
|
<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"
|
type="submit"
|
||||||
>
|
>
|
||||||
<i class="i-ph-arrow-up" />
|
<i class="i-ph-arrow-up" />
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -315,11 +315,7 @@ const componentToRender = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const shouldShowContextMenu = computed(() => {
|
const shouldShowContextMenu = computed(() => {
|
||||||
return !(
|
return !props.contentAttributes?.isUnsupported;
|
||||||
props.status === MESSAGE_STATUS.FAILED ||
|
|
||||||
props.status === MESSAGE_STATUS.PROGRESS ||
|
|
||||||
props.contentAttributes?.isUnsupported
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isBubble = computed(() => {
|
const isBubble = computed(() => {
|
||||||
@@ -344,12 +340,23 @@ const contextMenuEnabledOptions = computed(() => {
|
|||||||
const hasAttachments = !!(props.attachments && props.attachments.length > 0);
|
const hasAttachments = !!(props.attachments && props.attachments.length > 0);
|
||||||
|
|
||||||
const isOutgoing = props.messageType === MESSAGE_TYPES.OUTGOING;
|
const isOutgoing = props.messageType === MESSAGE_TYPES.OUTGOING;
|
||||||
|
const isFailedOrProcessing =
|
||||||
|
props.status === MESSAGE_STATUS.FAILED ||
|
||||||
|
props.status === MESSAGE_STATUS.PROGRESS;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
copy: hasText,
|
copy: hasText,
|
||||||
delete: hasText || hasAttachments,
|
delete:
|
||||||
cannedResponse: isOutgoing && hasText,
|
(hasText || hasAttachments) &&
|
||||||
replyTo: !props.private && props.inboxSupportsReplyTo.outgoing,
|
!isFailedOrProcessing &&
|
||||||
|
!isMessageDeleted.value,
|
||||||
|
cannedResponse: isOutgoing && hasText && !isMessageDeleted.value,
|
||||||
|
copyLink: !isFailedOrProcessing,
|
||||||
|
translate: !isFailedOrProcessing && !isMessageDeleted.value && hasText,
|
||||||
|
replyTo:
|
||||||
|
!props.private &&
|
||||||
|
props.inboxSupportsReplyTo.outgoing &&
|
||||||
|
!isFailedOrProcessing,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -499,8 +506,8 @@ provideMessageContext({
|
|||||||
<div
|
<div
|
||||||
class="[grid-area:bubble] flex"
|
class="[grid-area:bubble] flex"
|
||||||
:class="{
|
:class="{
|
||||||
'ltr:pl-8 rtl:pr-8 justify-end': orientation === ORIENTATION.RIGHT,
|
'ltr:ml-8 rtl:mr-8 justify-end': orientation === ORIENTATION.RIGHT,
|
||||||
'ltr:pr-8 rtl:pl-8': orientation === ORIENTATION.LEFT,
|
'ltr:mr-8 rtl:ml-8': orientation === ORIENTATION.LEFT,
|
||||||
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
|
'min-w-0': variant === MESSAGE_VARIANTS.EMAIL,
|
||||||
}"
|
}"
|
||||||
@contextmenu="openContextMenu($event)"
|
@contextmenu="openContextMenu($event)"
|
||||||
@@ -516,7 +523,7 @@ provideMessageContext({
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
|
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
v-if="isBubble && !isMessageDeleted"
|
v-if="isBubble"
|
||||||
:context-menu-position="contextMenuPosition"
|
:context-menu-position="contextMenuPosition"
|
||||||
:is-open="showContextMenu"
|
:is-open="showContextMenu"
|
||||||
:enabled-options="contextMenuEnabledOptions"
|
:enabled-options="contextMenuEnabledOptions"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useMapGetter, useStore } from 'dashboard/composables/store';
|
|||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useImpersonation } from 'dashboard/composables/useImpersonation';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownContainer,
|
DropdownContainer,
|
||||||
@@ -20,6 +21,8 @@ const currentUserAvailability = useMapGetter('getCurrentUserAvailability');
|
|||||||
const currentAccountId = useMapGetter('getCurrentAccountId');
|
const currentAccountId = useMapGetter('getCurrentAccountId');
|
||||||
const currentUserAutoOffline = useMapGetter('getCurrentUserAutoOffline');
|
const currentUserAutoOffline = useMapGetter('getCurrentUserAutoOffline');
|
||||||
|
|
||||||
|
const { isImpersonating } = useImpersonation();
|
||||||
|
|
||||||
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
|
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
|
||||||
const statusList = computed(() => {
|
const statusList = computed(() => {
|
||||||
return [
|
return [
|
||||||
@@ -46,6 +49,10 @@ const activeStatus = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function changeAvailabilityStatus(availability) {
|
function changeAvailabilityStatus(availability) {
|
||||||
|
if (isImpersonating.value) {
|
||||||
|
useAlert(t('PROFILE_SETTINGS.FORM.AVAILABILITY.IMPERSONATING_ERROR'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
store.dispatch('updateAvailability', {
|
store.dispatch('updateAvailability', {
|
||||||
availability,
|
availability,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
computed,
|
computed,
|
||||||
watch,
|
watch,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
|
||||||
defineEmits,
|
defineEmits,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
@@ -44,7 +43,7 @@ import {
|
|||||||
useSnakeCase,
|
useSnakeCase,
|
||||||
} from 'dashboard/composables/useTransformKeys';
|
} from 'dashboard/composables/useTransformKeys';
|
||||||
import { useEmitter } from 'dashboard/composables/emitter';
|
import { useEmitter } from 'dashboard/composables/emitter';
|
||||||
import { useEventListener, useScrollLock } from '@vueuse/core';
|
import { useEventListener } from '@vueuse/core';
|
||||||
|
|
||||||
import { emitter } from 'shared/helpers/mitt';
|
import { emitter } from 'shared/helpers/mitt';
|
||||||
|
|
||||||
@@ -87,12 +86,8 @@ const store = useStore();
|
|||||||
|
|
||||||
const conversationListRef = ref(null);
|
const conversationListRef = ref(null);
|
||||||
const conversationDynamicScroller = ref(null);
|
const conversationDynamicScroller = ref(null);
|
||||||
const conversationListScrollableElement = computed(
|
|
||||||
() => conversationDynamicScroller.value?.$el
|
provide('contextMenuElementTarget', conversationDynamicScroller);
|
||||||
);
|
|
||||||
const conversationListScrollLock = useScrollLock(
|
|
||||||
conversationListScrollableElement
|
|
||||||
);
|
|
||||||
|
|
||||||
const activeAssigneeTab = ref(wootConstants.ASSIGNEE_TYPE.ME);
|
const activeAssigneeTab = ref(wootConstants.ASSIGNEE_TYPE.ME);
|
||||||
const activeStatus = ref(wootConstants.STATUS_TYPE.OPEN);
|
const activeStatus = ref(wootConstants.STATUS_TYPE.OPEN);
|
||||||
@@ -746,7 +741,6 @@ function allSelectedConversationsStatus(status) {
|
|||||||
|
|
||||||
function onContextMenuToggle(state) {
|
function onContextMenuToggle(state) {
|
||||||
isContextMenuOpen.value = state;
|
isContextMenuOpen.value = state;
|
||||||
conversationListScrollLock.value = state;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelectAll(check) {
|
function toggleSelectAll(check) {
|
||||||
@@ -770,10 +764,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
conversationListScrollLock.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
provide('selectConversation', selectConversation);
|
provide('selectConversation', selectConversation);
|
||||||
provide('deSelectConversation', deSelectConversation);
|
provide('deSelectConversation', deSelectConversation);
|
||||||
provide('assignAgent', onAssignAgent);
|
provide('assignAgent', onAssignAgent);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { useImpersonation } from 'dashboard/composables/useImpersonation';
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
|
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
|
||||||
@@ -20,6 +21,10 @@ export default {
|
|||||||
AvailabilityStatusBadge,
|
AvailabilityStatusBadge,
|
||||||
NextButton,
|
NextButton,
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const { isImpersonating } = useImpersonation();
|
||||||
|
return { isImpersonating };
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isStatusMenuOpened: false,
|
isStatusMenuOpened: false,
|
||||||
@@ -73,6 +78,13 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
changeAvailabilityStatus(availability) {
|
changeAvailabilityStatus(availability) {
|
||||||
|
if (this.isImpersonating) {
|
||||||
|
useAlert(
|
||||||
|
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.IMPERSONATING_ERROR')
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isUpdating) {
|
if (this.isUpdating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, nextTick, useTemplateRef } from 'vue';
|
import {
|
||||||
import { useWindowSize, useElementBounding } from '@vueuse/core';
|
computed,
|
||||||
|
onMounted,
|
||||||
|
nextTick,
|
||||||
|
onUnmounted,
|
||||||
|
useTemplateRef,
|
||||||
|
inject,
|
||||||
|
} from 'vue';
|
||||||
|
import { useWindowSize, useElementBounding, useScrollLock } from '@vueuse/core';
|
||||||
|
|
||||||
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
|
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
|
||||||
|
|
||||||
@@ -11,27 +18,34 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const elementToLock = inject('contextMenuElementTarget', null);
|
||||||
|
|
||||||
const menuRef = useTemplateRef('menuRef');
|
const menuRef = useTemplateRef('menuRef');
|
||||||
|
|
||||||
|
const scrollLockElement = computed(() => {
|
||||||
|
if (!elementToLock?.value) return null;
|
||||||
|
return elementToLock.value?.$el;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLocked = useScrollLock(scrollLockElement);
|
||||||
|
|
||||||
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
||||||
const { width: menuWidth, height: menuHeight } = useElementBounding(menuRef);
|
const { width: menuWidth, height: menuHeight } = useElementBounding(menuRef);
|
||||||
|
|
||||||
const calculatePosition = (x, y, menuW, menuH, windowW, windowH) => {
|
const calculatePosition = (x, y, menuW, menuH, windowW, windowH) => {
|
||||||
|
const PADDING = 16;
|
||||||
// Initial position
|
// Initial position
|
||||||
let left = x;
|
let left = x;
|
||||||
let top = y;
|
let top = y;
|
||||||
|
|
||||||
// Boundary checks
|
// Boundary checks
|
||||||
const isOverflowingRight = left + menuW > windowW;
|
const isOverflowingRight = left + menuW > windowW - PADDING;
|
||||||
const isOverflowingBottom = top + menuH > windowH;
|
const isOverflowingBottom = top + menuH > windowH - PADDING;
|
||||||
|
|
||||||
// Adjust position if overflowing
|
// Adjust position if overflowing
|
||||||
if (isOverflowingRight) left = windowW - menuW;
|
if (isOverflowingRight) left = windowW - menuW - PADDING;
|
||||||
if (isOverflowingBottom) top = windowH - menuH;
|
if (isOverflowingBottom) top = windowH - menuH - PADDING;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
left: Math.max(0, left),
|
left: Math.max(PADDING, left),
|
||||||
top: Math.max(0, top),
|
top: Math.max(PADDING, top),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,8 +68,18 @@ const position = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
isLocked.value = true;
|
||||||
nextTick(() => menuRef.value?.focus());
|
nextTick(() => menuRef.value?.focus());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
isLocked.value = false;
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
isLocked.value = false;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -65,7 +89,7 @@ onMounted(() => {
|
|||||||
class="fixed outline-none z-[9999] cursor-pointer"
|
class="fixed outline-none z-[9999] cursor-pointer"
|
||||||
:style="position"
|
:style="position"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@blur="emit('close')"
|
@blur="handleClose"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -185,8 +185,17 @@ export default {
|
|||||||
contextMenuEnabledOptions() {
|
contextMenuEnabledOptions() {
|
||||||
return {
|
return {
|
||||||
copy: this.hasText,
|
copy: this.hasText,
|
||||||
delete: this.hasText || this.hasAttachments,
|
delete:
|
||||||
cannedResponse: this.isOutgoing && this.hasText,
|
(this.hasText || this.hasAttachments) &&
|
||||||
|
!this.isMessageDeleted &&
|
||||||
|
!this.isFailed,
|
||||||
|
cannedResponse:
|
||||||
|
this.isOutgoing && this.hasText && !this.isMessageDeleted,
|
||||||
|
copyLink: !this.isFailed || !this.isProcessing,
|
||||||
|
translate:
|
||||||
|
(!this.isFailed || !this.isProcessing) &&
|
||||||
|
!this.isMessageDeleted &&
|
||||||
|
this.hasText,
|
||||||
replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
|
replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -328,7 +337,7 @@ export default {
|
|||||||
return !this.sender.type || this.sender.type === 'agent_bot';
|
return !this.sender.type || this.sender.type === 'agent_bot';
|
||||||
},
|
},
|
||||||
shouldShowContextMenu() {
|
shouldShowContextMenu() {
|
||||||
return !(this.isFailed || this.isPending || this.isUnsupported);
|
return !this.isUnsupported;
|
||||||
},
|
},
|
||||||
showAvatar() {
|
showAvatar() {
|
||||||
if (this.isOutgoing || this.isTemplate) {
|
if (this.isOutgoing || this.isTemplate) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { ref } from 'vue';
|
import { ref, provide } from 'vue';
|
||||||
// composable
|
// composable
|
||||||
import { useConfig } from 'dashboard/composables/useConfig';
|
import { useConfig } from 'dashboard/composables/useConfig';
|
||||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||||
@@ -63,6 +63,7 @@ export default {
|
|||||||
emits: ['contactPanelToggle'],
|
emits: ['contactPanelToggle'],
|
||||||
setup() {
|
setup() {
|
||||||
const isPopOutReplyBox = ref(false);
|
const isPopOutReplyBox = ref(false);
|
||||||
|
const conversationPanelRef = ref(null);
|
||||||
const { isEnterprise } = useConfig();
|
const { isEnterprise } = useConfig();
|
||||||
|
|
||||||
const closePopOutReplyBox = () => {
|
const closePopOutReplyBox = () => {
|
||||||
@@ -98,6 +99,8 @@ export default {
|
|||||||
FEATURE_FLAGS.CHATWOOT_V4
|
FEATURE_FLAGS.CHATWOOT_V4
|
||||||
);
|
);
|
||||||
|
|
||||||
|
provide('contextMenuElementTarget', conversationPanelRef);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isEnterprise,
|
isEnterprise,
|
||||||
isPopOutReplyBox,
|
isPopOutReplyBox,
|
||||||
@@ -108,6 +111,7 @@ export default {
|
|||||||
fetchIntegrationsIfRequired,
|
fetchIntegrationsIfRequired,
|
||||||
fetchLabelSuggestions,
|
fetchLabelSuggestions,
|
||||||
showNextBubbles,
|
showNextBubbles,
|
||||||
|
conversationPanelRef,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -541,6 +545,7 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
<NextMessageList
|
<NextMessageList
|
||||||
v-if="showNextBubbles"
|
v-if="showNextBubbles"
|
||||||
|
ref="conversationPanelRef"
|
||||||
class="conversation-panel"
|
class="conversation-panel"
|
||||||
:current-user-id="currentUserId"
|
:current-user-id="currentUserId"
|
||||||
:first-unread-id="unReadMessages[0]?.id"
|
:first-unread-id="unReadMessages[0]?.id"
|
||||||
@@ -572,7 +577,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</NextMessageList>
|
</NextMessageList>
|
||||||
<ul v-else class="conversation-panel">
|
<ul v-else ref="conversationPanelRef" class="conversation-panel">
|
||||||
<transition name="slide-up">
|
<transition name="slide-up">
|
||||||
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
|
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
|
||||||
<li class="min-h-[4rem]">
|
<li class="min-h-[4rem]">
|
||||||
|
|||||||
@@ -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',
|
||||||
|
};
|
||||||
@@ -3,6 +3,9 @@ import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnec
|
|||||||
import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper';
|
import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import { emitter } from 'shared/helpers/mitt';
|
import { emitter } from 'shared/helpers/mitt';
|
||||||
|
import { useImpersonation } from 'dashboard/composables/useImpersonation';
|
||||||
|
|
||||||
|
const { isImpersonating } = useImpersonation();
|
||||||
|
|
||||||
class ActionCableConnector extends BaseActionCableConnector {
|
class ActionCableConnector extends BaseActionCableConnector {
|
||||||
constructor(app, pubsubToken) {
|
constructor(app, pubsubToken) {
|
||||||
@@ -52,6 +55,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onPresenceUpdate = data => {
|
onPresenceUpdate = data => {
|
||||||
|
if (isImpersonating.value) return;
|
||||||
this.app.$store.dispatch('contacts/updatePresence', data.contacts);
|
this.app.$store.dispatch('contacts/updatePresence', data.contacts);
|
||||||
this.app.$store.dispatch('agents/updatePresence', data.users);
|
this.app.$store.dispatch('agents/updatePresence', data.users);
|
||||||
this.app.$store.dispatch('setCurrentUserAvailability', data.users);
|
this.app.$store.dispatch('setCurrentUserAvailability', data.users);
|
||||||
|
|||||||
@@ -59,6 +59,11 @@
|
|||||||
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
"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": {
|
"FORM": {
|
||||||
"AVATAR": {
|
"AVATAR": {
|
||||||
"LABEL": "Bot avatar"
|
"LABEL": "Bot avatar"
|
||||||
|
|||||||
@@ -458,6 +458,10 @@
|
|||||||
"PLACEHOLDER": "Add Twitter"
|
"PLACEHOLDER": "Add Twitter"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"DELETE_CONTACT": {
|
||||||
|
"MESSAGE": "This action is permanent and irreversible.",
|
||||||
|
"BUTTON": "Delete now"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DETAILS": {
|
"DETAILS": {
|
||||||
@@ -467,7 +471,7 @@
|
|||||||
"DELETE_CONTACT": "Delete contact",
|
"DELETE_CONTACT": "Delete contact",
|
||||||
"DELETE_DIALOG": {
|
"DELETE_DIALOG": {
|
||||||
"TITLE": "Confirm Deletion",
|
"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",
|
"CONFIRM": "Yes, Delete",
|
||||||
"API": {
|
"API": {
|
||||||
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
||||||
|
|||||||
@@ -327,12 +327,14 @@
|
|||||||
"NAME": "Captain",
|
"NAME": "Captain",
|
||||||
"HEADER_KNOW_MORE": "Know more",
|
"HEADER_KNOW_MORE": "Know more",
|
||||||
"COPILOT": {
|
"COPILOT": {
|
||||||
|
"TITLE": "Copilot",
|
||||||
"SEND_MESSAGE": "Send message...",
|
"SEND_MESSAGE": "Send message...",
|
||||||
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
|
"EMPTY_MESSAGE": "There was an error generating the response. Please try again.",
|
||||||
"LOADER": "Captain is thinking",
|
"LOADER": "Captain is thinking",
|
||||||
"YOU": "You",
|
"YOU": "You",
|
||||||
"USE": "Use this",
|
"USE": "Use this",
|
||||||
"RESET": "Reset",
|
"RESET": "Reset",
|
||||||
|
"SHOW_STEPS": "Show steps",
|
||||||
"SELECT_ASSISTANT": "Select Assistant",
|
"SELECT_ASSISTANT": "Select Assistant",
|
||||||
"PROMPTS": {
|
"PROMPTS": {
|
||||||
"SUMMARIZE": {
|
"SUMMARIZE": {
|
||||||
@@ -365,7 +367,6 @@
|
|||||||
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
|
||||||
},
|
},
|
||||||
"ENTERPRISE_PAYWALL": {
|
"ENTERPRISE_PAYWALL": {
|
||||||
"AVAILABLE_ON": "Captain AI feature is only available in a paid plan.",
|
|
||||||
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
|
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
|
||||||
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
|
||||||
},
|
},
|
||||||
@@ -408,6 +409,10 @@
|
|||||||
"PLACEHOLDER": "Enter assistant name",
|
"PLACEHOLDER": "Enter assistant name",
|
||||||
"ERROR": "The name is required"
|
"ERROR": "The name is required"
|
||||||
},
|
},
|
||||||
|
"TEMPERATURE": {
|
||||||
|
"LABEL": "Response Temperature",
|
||||||
|
"DESCRIPTION": "Adjust how creative or restrictive the assistant's responses should be. Lower values produce more focused and deterministic responses, while higher values allow for more creative and varied outputs."
|
||||||
|
},
|
||||||
"DESCRIPTION": {
|
"DESCRIPTION": {
|
||||||
"LABEL": "Description",
|
"LABEL": "Description",
|
||||||
"PLACEHOLDER": "Enter assistant description",
|
"PLACEHOLDER": "Enter assistant description",
|
||||||
|
|||||||
@@ -185,7 +185,8 @@
|
|||||||
"OFFLINE": "Offline"
|
"OFFLINE": "Offline"
|
||||||
},
|
},
|
||||||
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
|
"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": {
|
"EMAIL": {
|
||||||
"LABEL": "Your email address",
|
"LABEL": "Your email address",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default {
|
|||||||
emits: ['open', 'close', 'replyTo'],
|
emits: ['open', 'close', 'replyTo'],
|
||||||
setup() {
|
setup() {
|
||||||
const { getPlainText } = useMessageFormatter();
|
const { getPlainText } = useMessageFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getPlainText,
|
getPlainText,
|
||||||
};
|
};
|
||||||
@@ -167,7 +168,7 @@ export default {
|
|||||||
</woot-modal>
|
</woot-modal>
|
||||||
<!-- Confirm Deletion -->
|
<!-- Confirm Deletion -->
|
||||||
<woot-delete-modal
|
<woot-delete-modal
|
||||||
v-if="showDeleteModal"
|
v-if="showDeleteModal && enabledOptions['delete']"
|
||||||
v-model:show="showDeleteModal"
|
v-model:show="showDeleteModal"
|
||||||
class="context-menu--delete-modal"
|
class="context-menu--delete-modal"
|
||||||
:on-close="closeDeleteModal"
|
:on-close="closeDeleteModal"
|
||||||
@@ -212,7 +213,7 @@ export default {
|
|||||||
@click.stop="handleCopy"
|
@click.stop="handleCopy"
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
v-if="enabledOptions['copy']"
|
v-if="enabledOptions['translate']"
|
||||||
:option="{
|
:option="{
|
||||||
icon: 'translate',
|
icon: 'translate',
|
||||||
label: $t('CONVERSATION.CONTEXT_MENU.TRANSLATE'),
|
label: $t('CONVERSATION.CONTEXT_MENU.TRANSLATE'),
|
||||||
@@ -222,6 +223,7 @@ export default {
|
|||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
v-if="enabledOptions['copyLink']"
|
||||||
:option="{
|
:option="{
|
||||||
icon: 'link',
|
icon: 'link',
|
||||||
label: $t('CONVERSATION.CONTEXT_MENU.COPY_PERMALINK'),
|
label: $t('CONVERSATION.CONTEXT_MENU.COPY_PERMALINK'),
|
||||||
|
|||||||
+131
-45
@@ -5,12 +5,15 @@ import { useAlert } from 'dashboard/composables';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { required, helpers, url } from '@vuelidate/validators';
|
import { required, helpers, url } from '@vuelidate/validators';
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
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 Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
import Input from 'dashboard/components-next/input/Input.vue';
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||||
|
import AccessToken from 'dashboard/routes/dashboard/settings/profile/AccessToken.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
type: {
|
type: {
|
||||||
@@ -42,6 +45,9 @@ const formState = reactive({
|
|||||||
botAvatarUrl: '',
|
botAvatarUrl: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [showAccessToken, toggleAccessToken] = useToggle();
|
||||||
|
const accessToken = ref('');
|
||||||
|
|
||||||
const v$ = useVuelidate(
|
const v$ = useVuelidate(
|
||||||
{
|
{
|
||||||
botName: {
|
botName: {
|
||||||
@@ -70,11 +76,22 @@ const isLoading = computed(() =>
|
|||||||
: uiFlags.value.isUpdating
|
: uiFlags.value.isUpdating
|
||||||
);
|
);
|
||||||
|
|
||||||
const dialogTitle = computed(() =>
|
const dialogTitle = computed(() => {
|
||||||
props.type === MODAL_TYPES.CREATE
|
if (showAccessToken.value) {
|
||||||
|
return t('AGENT_BOTS.ACCESS_TOKEN.TITLE');
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.type === MODAL_TYPES.CREATE
|
||||||
? t('AGENT_BOTS.ADD.TITLE')
|
? 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(() =>
|
const confirmButtonLabel = computed(() =>
|
||||||
props.type === MODAL_TYPES.CREATE
|
props.type === MODAL_TYPES.CREATE
|
||||||
@@ -90,6 +107,13 @@ const botUrlError = computed(() =>
|
|||||||
v$.value.botUrl.$error ? v$.value.botUrl.$errors[0]?.$message : ''
|
v$.value.botUrl.$error ? v$.value.botUrl.$errors[0]?.$message : ''
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const showAccessTokenInput = computed(
|
||||||
|
() =>
|
||||||
|
showAccessToken.value ||
|
||||||
|
props.type === MODAL_TYPES.EDIT ||
|
||||||
|
accessToken.value
|
||||||
|
);
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
Object.assign(formState, {
|
Object.assign(formState, {
|
||||||
botName: '',
|
botName: '',
|
||||||
@@ -128,6 +152,7 @@ const handleAvatarDelete = async () => {
|
|||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
v$.value.$touch();
|
v$.value.$touch();
|
||||||
if (v$.value.$invalid) return;
|
if (v$.value.$invalid) return;
|
||||||
|
if (showAccessToken.value) return;
|
||||||
|
|
||||||
const botData = {
|
const botData = {
|
||||||
name: formState.botName,
|
name: formState.botName,
|
||||||
@@ -144,7 +169,7 @@ const handleSubmit = async () => {
|
|||||||
? botData
|
? botData
|
||||||
: { id: props.selectedBot.id, data: botData };
|
: { id: props.selectedBot.id, data: botData };
|
||||||
|
|
||||||
await store.dispatch(
|
const response = await store.dispatch(
|
||||||
`agentBots/${isCreate ? 'create' : 'update'}`,
|
`agentBots/${isCreate ? 'create' : 'update'}`,
|
||||||
actionPayload
|
actionPayload
|
||||||
);
|
);
|
||||||
@@ -154,7 +179,21 @@ const handleSubmit = async () => {
|
|||||||
: t('AGENT_BOTS.EDIT.API.SUCCESS_MESSAGE');
|
: t('AGENT_BOTS.EDIT.API.SUCCESS_MESSAGE');
|
||||||
useAlert(alertKey);
|
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();
|
resetForm();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorKey = isCreate
|
const errorKey = isCreate
|
||||||
@@ -166,17 +205,43 @@ const handleSubmit = async () => {
|
|||||||
|
|
||||||
const initializeForm = () => {
|
const initializeForm = () => {
|
||||||
if (props.selectedBot && Object.keys(props.selectedBot).length) {
|
if (props.selectedBot && Object.keys(props.selectedBot).length) {
|
||||||
const { name, description, outgoing_url, thumbnail, bot_config } =
|
const {
|
||||||
props.selectedBot;
|
name,
|
||||||
|
description,
|
||||||
|
outgoing_url: botUrl,
|
||||||
|
thumbnail,
|
||||||
|
bot_config: botConfig,
|
||||||
|
access_token: botAccessToken,
|
||||||
|
} = props.selectedBot;
|
||||||
formState.botName = name || '';
|
formState.botName = name || '';
|
||||||
formState.botDescription = description || '';
|
formState.botDescription = description || '';
|
||||||
formState.botUrl = outgoing_url || bot_config?.webhook_url || '';
|
formState.botUrl = botUrl || botConfig?.webhook_url || '';
|
||||||
formState.botAvatarUrl = thumbnail || '';
|
formState.botAvatarUrl = thumbnail || '';
|
||||||
|
|
||||||
|
if (botAccessToken && props.type === MODAL_TYPES.EDIT) {
|
||||||
|
accessToken.value = botAccessToken;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
resetForm();
|
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 });
|
watch(() => props.selectedBot, initializeForm, { immediate: true, deep: true });
|
||||||
|
|
||||||
defineExpose({ dialogRef });
|
defineExpose({ dialogRef });
|
||||||
@@ -187,48 +252,68 @@ defineExpose({ dialogRef });
|
|||||||
ref="dialogRef"
|
ref="dialogRef"
|
||||||
type="edit"
|
type="edit"
|
||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
|
:description="dialogDescription"
|
||||||
:show-cancel-button="false"
|
:show-cancel-button="false"
|
||||||
:show-confirm-button="false"
|
:show-confirm-button="false"
|
||||||
@close="v$.$reset()"
|
@close="closeModal"
|
||||||
>
|
>
|
||||||
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
<form class="flex flex-col gap-4" @submit.prevent="handleSubmit">
|
||||||
<div class="mb-2 flex flex-col items-start">
|
<div
|
||||||
<span class="mb-2 text-sm font-medium text-n-slate-12">
|
v-if="!showAccessToken || type === MODAL_TYPES.EDIT"
|
||||||
{{ $t('AGENT_BOTS.FORM.AVATAR.LABEL') }}
|
class="flex flex-col gap-4"
|
||||||
</span>
|
>
|
||||||
<Avatar
|
<div class="mb-2 flex flex-col items-start">
|
||||||
:src="formState.botAvatarUrl"
|
<span class="mb-2 text-sm font-medium text-n-slate-12">
|
||||||
:name="formState.botName"
|
{{ $t('AGENT_BOTS.FORM.AVATAR.LABEL') }}
|
||||||
:size="68"
|
</span>
|
||||||
allow-upload
|
<Avatar
|
||||||
@upload="handleImageUpload"
|
:src="formState.botAvatarUrl"
|
||||||
@delete="handleAvatarDelete"
|
: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>
|
</div>
|
||||||
|
|
||||||
<Input
|
<div v-if="showAccessTokenInput" class="flex flex-col gap-1">
|
||||||
v-model="formState.botName"
|
<label
|
||||||
:label="$t('AGENT_BOTS.FORM.NAME.LABEL')"
|
v-if="type === MODAL_TYPES.EDIT"
|
||||||
:placeholder="$t('AGENT_BOTS.FORM.NAME.PLACEHOLDER')"
|
class="mb-0.5 text-sm font-medium text-n-slate-12"
|
||||||
:message="botNameError"
|
>
|
||||||
:message-type="botNameError ? 'error' : 'info'"
|
{{ $t('AGENT_BOTS.ACCESS_TOKEN.TITLE') }}
|
||||||
@blur="v$.botName.$touch()"
|
</label>
|
||||||
/>
|
<AccessToken :value="accessToken" @on-copy="onCopyToken" />
|
||||||
|
</div>
|
||||||
<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 class="flex items-center justify-end w-full gap-2 px-0 py-2">
|
<div class="flex items-center justify-end w-full gap-2 px-0 py-2">
|
||||||
<NextButton
|
<NextButton
|
||||||
@@ -236,9 +321,10 @@ defineExpose({ dialogRef });
|
|||||||
slate
|
slate
|
||||||
type="reset"
|
type="reset"
|
||||||
:label="$t('AGENT_BOTS.FORM.CANCEL')"
|
:label="$t('AGENT_BOTS.FORM.CANCEL')"
|
||||||
@click="dialogRef.close()"
|
@click="onClickClose()"
|
||||||
/>
|
/>
|
||||||
<NextButton
|
<NextButton
|
||||||
|
v-if="!showAccessToken"
|
||||||
type="submit"
|
type="submit"
|
||||||
data-testid="label-submit"
|
data-testid="label-submit"
|
||||||
:label="confirmButtonLabel"
|
:label="confirmButtonLabel"
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export default {
|
|||||||
<div class="flex flex-col h-auto overflow-auto integration-hooks">
|
<div class="flex flex-col h-auto overflow-auto integration-hooks">
|
||||||
<woot-modal-header
|
<woot-modal-header
|
||||||
:header-title="integration.name"
|
:header-title="integration.name"
|
||||||
:header-content="integration.short_description || integration.description"
|
:header-content="integration.short_description"
|
||||||
/>
|
/>
|
||||||
<FormKit
|
<FormKit
|
||||||
v-model="values"
|
v-model="values"
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const onClick = () => {
|
|||||||
<template #masked>
|
<template #masked>
|
||||||
<button
|
<button
|
||||||
class="absolute top-1.5 ltr:right-0.5 rtl:left-0.5"
|
class="absolute top-1.5 ltr:right-0.5 rtl:left-0.5"
|
||||||
|
type="button"
|
||||||
@click="toggleMasked"
|
@click="toggleMasked"
|
||||||
>
|
>
|
||||||
<fluent-icon :icon="maskIcon" :size="16" />
|
<fluent-icon :icon="maskIcon" :size="16" />
|
||||||
@@ -46,7 +47,7 @@ const onClick = () => {
|
|||||||
</template>
|
</template>
|
||||||
</woot-input>
|
</woot-input>
|
||||||
<FormButton
|
<FormButton
|
||||||
type="submit"
|
type="button"
|
||||||
size="large"
|
size="large"
|
||||||
icon="text-copy"
|
icon="text-copy"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import types from '../mutation-types';
|
|||||||
import authAPI from '../../api/auth';
|
import authAPI from '../../api/auth';
|
||||||
|
|
||||||
import { setUser, clearCookiesOnLogout } from '../utils/api';
|
import { setUser, clearCookiesOnLogout } from '../utils/api';
|
||||||
|
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||||
|
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
currentUser: {
|
currentUser: {
|
||||||
@@ -145,8 +147,15 @@ export const actions = {
|
|||||||
updateUISettings: async ({ commit }, params) => {
|
updateUISettings: async ({ commit }, params) => {
|
||||||
try {
|
try {
|
||||||
commit(types.SET_CURRENT_USER_UI_SETTINGS, params);
|
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) {
|
} catch (error) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import fromUnixTime from 'date-fns/fromUnixTime';
|
|||||||
import differenceInDays from 'date-fns/differenceInDays';
|
import differenceInDays from 'date-fns/differenceInDays';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||||
|
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||||
|
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||||
import { emitter } from 'shared/helpers/mitt';
|
import { emitter } from 'shared/helpers/mitt';
|
||||||
import {
|
import {
|
||||||
ANALYTICS_IDENTITY,
|
ANALYTICS_IDENTITY,
|
||||||
@@ -44,6 +46,10 @@ export const clearLocalStorageOnLogout = () => {
|
|||||||
LocalStorage.remove(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES);
|
LocalStorage.remove(LOCAL_STORAGE_KEYS.DRAFT_MESSAGES);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const clearSessionStorageOnLogout = () => {
|
||||||
|
SessionStorage.remove(SESSION_STORAGE_KEYS.IMPERSONATION_USER);
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteIndexedDBOnLogout = async () => {
|
export const deleteIndexedDBOnLogout = async () => {
|
||||||
let dbs = [];
|
let dbs = [];
|
||||||
try {
|
try {
|
||||||
@@ -75,6 +81,7 @@ export const clearCookiesOnLogout = () => {
|
|||||||
emitter.emit(ANALYTICS_RESET);
|
emitter.emit(ANALYTICS_RESET);
|
||||||
clearBrowserSessionCookies();
|
clearBrowserSessionCookies();
|
||||||
clearLocalStorageOnLogout();
|
clearLocalStorageOnLogout();
|
||||||
|
clearSessionStorageOnLogout();
|
||||||
const globalConfig = window.globalConfig || {};
|
const globalConfig = window.globalConfig || {};
|
||||||
const logoutRedirectLink = globalConfig.LOGOUT_REDIRECT_LINK || '/';
|
const logoutRedirectLink = globalConfig.LOGOUT_REDIRECT_LINK || '/';
|
||||||
window.location = logoutRedirectLink;
|
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 { useAlert } from 'dashboard/composables';
|
||||||
import { required, email } from '@vuelidate/validators';
|
import { required, email } from '@vuelidate/validators';
|
||||||
import { useVuelidate } from '@vuelidate/core';
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||||
|
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||||
// mixins
|
// mixins
|
||||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||||
|
|
||||||
@@ -21,6 +22,8 @@ const ERROR_MESSAGES = {
|
|||||||
'business-account-only': 'LOGIN.OAUTH.BUSINESS_ACCOUNTS_ONLY',
|
'business-account-only': 'LOGIN.OAUTH.BUSINESS_ACCOUNTS_ONLY',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const IMPERSONATION_URL_SEARCH_KEY = 'impersonation';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FormInput,
|
FormInput,
|
||||||
@@ -112,6 +115,14 @@ export default {
|
|||||||
this.loginApi.message = message;
|
this.loginApi.message = message;
|
||||||
useAlert(this.loginApi.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() {
|
submitLogin() {
|
||||||
this.loginApi.hasErrored = false;
|
this.loginApi.hasErrored = false;
|
||||||
this.loginApi.showLoading = true;
|
this.loginApi.showLoading = true;
|
||||||
@@ -128,6 +139,7 @@ export default {
|
|||||||
|
|
||||||
login(credentials)
|
login(credentials)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
this.handleImpersonation();
|
||||||
this.showAlertMessage(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
|
this.showAlertMessage(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
|
||||||
})
|
})
|
||||||
.catch(response => {
|
.catch(response => {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
class Internal::DeleteAccountsJob < ApplicationJob
|
||||||
|
queue_as :scheduled_jobs
|
||||||
|
|
||||||
|
def perform
|
||||||
|
delete_expired_accounts
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def delete_expired_accounts
|
||||||
|
accounts_pending_deletion.each do |account|
|
||||||
|
AccountDeletionService.new(account: account).perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def accounts_pending_deletion
|
||||||
|
Account.where("custom_attributes->>'marked_for_deletion_at' IS NOT NULL")
|
||||||
|
.select { |account| deletion_period_expired?(account) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def deletion_period_expired?(account)
|
||||||
|
deletion_time = account.custom_attributes['marked_for_deletion_at']
|
||||||
|
return false if deletion_time.blank?
|
||||||
|
|
||||||
|
DateTime.parse(deletion_time) <= Time.current
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -204,3 +204,5 @@ class ActionCableListener < BaseListener
|
|||||||
::ActionCableBroadcastJob.perform_later(tokens.uniq, event_name, payload)
|
::ActionCableBroadcastJob.perform_later(tokens.uniq, event_name, payload)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ActionCableListener.prepend_mod_with('ActionCableListener')
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ApplicationMailbox < ActionMailbox::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def in_reply_to_matches?(in_reply_to)
|
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
|
end
|
||||||
|
|
||||||
# checks if follow this pattern send it to reply_mailbox
|
# checks if follow this pattern send it to reply_mailbox
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
class AdministratorNotifications::AccountComplianceMailer < AdministratorNotifications::BaseMailer
|
||||||
|
def account_deleted(account)
|
||||||
|
return if instance_admin_email.blank?
|
||||||
|
|
||||||
|
subject = subject_for(account)
|
||||||
|
meta = build_meta(account)
|
||||||
|
|
||||||
|
send_notification(subject, to: instance_admin_email, meta: meta)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_meta(account)
|
||||||
|
deleted_users = params[:soft_deleted_users] || []
|
||||||
|
|
||||||
|
user_info_list = deleted_users.map do |user|
|
||||||
|
{
|
||||||
|
'user_id' => user[:id].to_s,
|
||||||
|
'user_email' => user[:original_email].to_s
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
{
|
||||||
|
'instance_url' => instance_url,
|
||||||
|
'account_id' => account.id,
|
||||||
|
'account_name' => account.name,
|
||||||
|
'deleted_at' => format_time(Time.current.iso8601),
|
||||||
|
'deletion_reason' => account.custom_attributes['marked_for_deletion_reason'] || 'not specified',
|
||||||
|
'marked_for_deletion_at' => format_time(account.custom_attributes['marked_for_deletion_at']),
|
||||||
|
'soft_deleted_users' => user_info_list,
|
||||||
|
'deleted_user_count' => user_info_list.size
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_time(time_string)
|
||||||
|
return 'not specified' if time_string.blank?
|
||||||
|
|
||||||
|
Time.zone.parse(time_string).strftime('%B %d, %Y %H:%M:%S %Z')
|
||||||
|
end
|
||||||
|
|
||||||
|
def subject_for(account)
|
||||||
|
"Account Deletion Notice for #{account.id} - #{account.name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def instance_admin_email
|
||||||
|
GlobalConfig.get('CHATWOOT_INSTANCE_ADMIN_EMAIL')['CHATWOOT_INSTANCE_ADMIN_EMAIL']
|
||||||
|
end
|
||||||
|
|
||||||
|
def instance_url
|
||||||
|
ENV.fetch('FRONTEND_URL', 'not available')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -97,8 +97,8 @@ class Account < ApplicationRecord
|
|||||||
|
|
||||||
has_one_attached :contacts_export
|
has_one_attached :contacts_export
|
||||||
|
|
||||||
enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h
|
enum :locale, LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h, prefix: true
|
||||||
enum status: { active: 0, suspended: 1 }
|
enum :status, { active: 0, suspended: 1 }
|
||||||
|
|
||||||
scope :with_auto_resolve, -> { where("(settings ->> 'auto_resolve_after')::int IS NOT NULL") }
|
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}"
|
"#{ENV.fetch('FRONTEND_URL', nil)}/app/login?email=#{encoded_email}&sso_auth_token=#{generate_sso_auth_token}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def generate_sso_link_with_impersonation
|
||||||
|
"#{generate_sso_link}&impersonation=true"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def sso_token_key(token)
|
def sso_token_key(token)
|
||||||
|
|||||||
@@ -128,6 +128,12 @@ class Conversation < ApplicationRecord
|
|||||||
additional_attributes&.dig('conversation_language')
|
additional_attributes&.dig('conversation_language')
|
||||||
end
|
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
|
def last_activity_at
|
||||||
self[:last_activity_at] || created_at
|
self[:last_activity_at] || created_at
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
# index_email_templates_on_name_and_account_id (name,account_id) UNIQUE
|
# index_email_templates_on_name_and_account_id (name,account_id) UNIQUE
|
||||||
#
|
#
|
||||||
class EmailTemplate < ApplicationRecord
|
class EmailTemplate < ApplicationRecord
|
||||||
enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h
|
enum :locale, LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h, prefix: true
|
||||||
enum template_type: { layout: 0, content: 1 }
|
enum :template_type, { layout: 0, content: 1 }
|
||||||
belongs_to :account, optional: true
|
belongs_to :account, optional: true
|
||||||
|
|
||||||
validates :name, uniqueness: { scope: :account }
|
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
|
# 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 ME : fixes breakage of installation config. we need to migrate.
|
||||||
# Fix configuration in application.rb
|
# Fix configuration in application.rb
|
||||||
serialize :serialized_value, ActiveSupport::HashWithIndifferentAccess
|
serialize :serialized_value, coder: YAML, type: ActiveSupport::HashWithIndifferentAccess
|
||||||
|
|
||||||
before_validation :set_lock
|
before_validation :set_lock
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
@@ -32,6 +32,10 @@ class InstallationConfig < ApplicationRecord
|
|||||||
after_commit :clear_cache
|
after_commit :clear_cache
|
||||||
|
|
||||||
def value
|
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]
|
serialized_value[:value]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ class Integrations::App
|
|||||||
def action
|
def action
|
||||||
case params[:id]
|
case params[:id]
|
||||||
when 'slack'
|
when 'slack'
|
||||||
"#{params[:action]}&client_id=#{ENV.fetch('SLACK_CLIENT_ID', nil)}&redirect_uri=#{self.class.slack_integration_url}"
|
client_id = GlobalConfigService.load('SLACK_CLIENT_ID', nil)
|
||||||
|
"#{params[:action]}&client_id=#{client_id}&redirect_uri=#{self.class.slack_integration_url}"
|
||||||
when 'linear'
|
when 'linear'
|
||||||
build_linear_action
|
build_linear_action
|
||||||
else
|
else
|
||||||
@@ -50,7 +51,7 @@ class Integrations::App
|
|||||||
def active?(account)
|
def active?(account)
|
||||||
case params[:id]
|
case params[:id]
|
||||||
when 'slack'
|
when 'slack'
|
||||||
ENV['SLACK_CLIENT_SECRET'].present?
|
GlobalConfigService.load('SLACK_CLIENT_SECRET', nil).present?
|
||||||
when 'linear'
|
when 'linear'
|
||||||
GlobalConfigService.load('LINEAR_CLIENT_ID', nil).present?
|
GlobalConfigService.load('LINEAR_CLIENT_ID', nil).present?
|
||||||
when 'shopify'
|
when 'shopify'
|
||||||
|
|||||||
@@ -185,7 +185,13 @@ class Message < ApplicationRecord
|
|||||||
# move this to a presenter
|
# move this to a presenter
|
||||||
return self[:content] if !input_csat? || inbox.web_widget?
|
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
|
end
|
||||||
|
|
||||||
def email_notifiable_message?
|
def email_notifiable_message?
|
||||||
|
|||||||
+2
-2
@@ -109,8 +109,8 @@ class User < ApplicationRecord
|
|||||||
self.email = email.try(:downcase)
|
self.email = email.try(:downcase)
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_devise_notification(notification, *args)
|
def send_devise_notification(notification, *)
|
||||||
devise_mailer.with(account: Current.account).send(notification, self, *args).deliver_later
|
devise_mailer.with(account: Current.account).send(notification, self, *).deliver_later
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_password_and_uid
|
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
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
class AccountDeletionService
|
||||||
|
attr_reader :account, :soft_deleted_users
|
||||||
|
|
||||||
|
def initialize(account:)
|
||||||
|
@account = account
|
||||||
|
@soft_deleted_users = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform
|
||||||
|
Rails.logger.info("Deleting account #{account.id} - #{account.name} that was marked for deletion")
|
||||||
|
|
||||||
|
soft_delete_orphaned_users
|
||||||
|
send_compliance_notification
|
||||||
|
DeleteObjectJob.perform_later(account)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def send_compliance_notification
|
||||||
|
AdministratorNotifications::AccountComplianceMailer.with(
|
||||||
|
account: account,
|
||||||
|
soft_deleted_users: soft_deleted_users
|
||||||
|
).account_deleted(account).deliver_later
|
||||||
|
end
|
||||||
|
|
||||||
|
def soft_delete_orphaned_users
|
||||||
|
account.users.each do |user|
|
||||||
|
# Find all account_users for this user excluding the current account
|
||||||
|
other_accounts = user.account_users.where.not(account_id: account.id).count
|
||||||
|
|
||||||
|
# If user has no other accounts, soft delete them
|
||||||
|
next unless other_accounts.zero?
|
||||||
|
|
||||||
|
# Soft delete user by appending -deleted.com to email
|
||||||
|
original_email = user.email
|
||||||
|
user.email = "#{original_email}-deleted.com"
|
||||||
|
user.skip_reconfirmation!
|
||||||
|
user.save!
|
||||||
|
|
||||||
|
user_info = {
|
||||||
|
id: user.id.to_s,
|
||||||
|
original_email: original_email
|
||||||
|
}
|
||||||
|
|
||||||
|
soft_deleted_users << user_info
|
||||||
|
|
||||||
|
Rails.logger.info("Soft deleted user #{user.id} with email #{original_email}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -28,16 +28,6 @@ class Conversations::FilterService < FilterService
|
|||||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :messages, :contact_inbox
|
: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::PermissionFilterService.new(
|
||||||
conversations,
|
conversations,
|
||||||
@user,
|
@user,
|
||||||
|
|||||||
@@ -8,9 +8,23 @@ class Conversations::PermissionFilterService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
# The base implementation simply returns all conversations
|
return conversations if user_role == 'administrator'
|
||||||
# Enterprise edition extends this with permission-based filtering
|
|
||||||
conversations
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ class Crm::Leadsquared::LeadFinderService
|
|||||||
def find_by_phone_number(contact)
|
def find_by_phone_number(contact)
|
||||||
return if contact.phone_number.blank?
|
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
|
end
|
||||||
|
|
||||||
def search_by_field(value)
|
def search_by_field(value)
|
||||||
|
|||||||
@@ -20,11 +20,29 @@ class Crm::Leadsquared::Mappers::ContactMapper
|
|||||||
'FirstName' => contact.name.presence,
|
'FirstName' => contact.name.presence,
|
||||||
'LastName' => contact.last_name.presence,
|
'LastName' => contact.last_name.presence,
|
||||||
'EmailAddress' => contact.email.presence,
|
'EmailAddress' => contact.email.presence,
|
||||||
'Mobile' => contact.phone_number.presence,
|
'Mobile' => formatted_phone_number,
|
||||||
'Source' => brand_name
|
'Source' => brand_name
|
||||||
}.compact
|
}.compact
|
||||||
end
|
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
|
def brand_name
|
||||||
::GlobalConfig.get('BRAND_NAME')['BRAND_NAME'] || 'Chatwoot'
|
::GlobalConfig.get('BRAND_NAME')['BRAND_NAME'] || 'Chatwoot'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class MessageTemplates::HookExecutionService
|
|||||||
return false unless message.incoming?
|
return false unless message.incoming?
|
||||||
# prevents sending out-of-office message if an agent has sent a message in last 5 minutes
|
# 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
|
# 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?
|
inbox.out_of_office? && conversation.messages.today.template.empty? && inbox.out_of_office_message.present?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ class MessageTemplates::Template::CsatSurvey
|
|||||||
private
|
private
|
||||||
|
|
||||||
delegate :contact, :account, :inbox, to: :conversation
|
delegate :contact, :account, :inbox, to: :conversation
|
||||||
delegate :csat_config, to: :inbox
|
|
||||||
|
|
||||||
def should_send_csat_survey?
|
def should_send_csat_survey?
|
||||||
return true unless survey_rules_configured?
|
return true unless survey_rules_configured?
|
||||||
|
|||||||
@@ -137,14 +137,19 @@ class Twilio::IncomingMessageService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def download_with_auth(media_url)
|
def download_with_auth(media_url)
|
||||||
Down.download(
|
auth_credentials = if twilio_channel.api_key_sid.present?
|
||||||
media_url,
|
# When using api_key_sid, the auth token should be the api_secret_key
|
||||||
http_basic_authentication: [twilio_channel.account_sid, twilio_channel.auth_token || twilio_channel.api_key_sid]
|
[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
|
end
|
||||||
|
|
||||||
def handle_download_attachment_error(error, media_url)
|
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)
|
Down.download(media_url)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.info "Error downloading attachment from Twilio: #{e.message}: Skipping"
|
Rails.logger.info "Error downloading attachment from Twilio: #{e.message}: Skipping"
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
<p>Hello,</p>
|
||||||
|
|
||||||
|
<p>This is a notification to inform you that an account has been permanently deleted from your Chatwoot instance.</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Chatwoot Installation:</strong> {{ meta.instance_url }}<br>
|
||||||
|
<strong>Account ID:</strong> {{ meta.account_id }}<br>
|
||||||
|
<strong>Account Name:</strong> {{ meta.account_name }}<br>
|
||||||
|
<strong>Deleted At:</strong> {{ meta.deleted_at }}<br>
|
||||||
|
<strong>Marked for Deletion at:</strong> {{ meta.marked_for_deletion_at }}<br>
|
||||||
|
<strong>Deletion Reason:</strong> {{ meta.deletion_reason }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if meta.deleted_user_count > 0 %}
|
||||||
|
<p>
|
||||||
|
<strong>Deleted Users ({{ meta.deleted_user_count }}):</strong><br>
|
||||||
|
{% for user in meta.soft_deleted_users %}
|
||||||
|
User ID: {{ user.user_id }}, Email: {{ user.user_email }}{% unless forloop.last %}<br>{% endunless %}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
<strong>Deleted Users:</strong> None
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>This email serves as a record for compliance purposes.</p>
|
||||||
|
|
||||||
|
<p>Thank you,<br>
|
||||||
|
Chatwoot System</p>
|
||||||
@@ -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
|
||||||
@@ -159,4 +159,7 @@
|
|||||||
<symbol id="icon-shopify" viewBox="0 0 32 32">
|
<symbol id="icon-shopify" viewBox="0 0 32 32">
|
||||||
<path fill="currentColor" d="m20.448 31.974l9.625-2.083s-3.474-23.484-3.5-23.641s-.156-.255-.281-.255c-.13 0-2.573-.182-2.573-.182s-1.703-1.698-1.922-1.88a.4.4 0 0 0-.161-.099l-1.219 28.141zm-4.833-16.901s-1.083-.563-2.365-.563c-1.932 0-2.005 1.203-2.005 1.521c0 1.641 4.318 2.286 4.318 6.172c0 3.057-1.922 5.01-4.542 5.01c-3.141 0-4.719-1.953-4.719-1.953l.859-2.781s1.661 1.422 3.042 1.422c.901 0 1.302-.724 1.302-1.245c0-2.156-3.542-2.255-3.542-5.807c-.047-2.984 2.094-5.891 6.438-5.891c1.677 0 2.5.479 2.5.479l-1.26 3.625zm-.719-13.969c.177 0 .359.052.536.182c-1.313.62-2.75 2.188-3.344 5.323a76 76 0 0 1-2.516.771c.688-2.38 2.359-6.26 5.323-6.26zm1.646 3.932v.182c-1.005.307-2.115.646-3.193.979c.62-2.37 1.776-3.526 2.781-3.958c.255.667.411 1.568.411 2.797zm.718-2.973c.922.094 1.521 1.151 1.901 2.339c-.464.151-.979.307-1.542.484v-.333c0-1.005-.13-1.828-.359-2.495zm3.99 1.718c-.031 0-.083.026-.104.026c-.026 0-.385.099-.953.281C19.63 2.442 18.625.927 16.849.927h-.156C16.183.281 15.558 0 15.021 0c-4.141 0-6.12 5.172-6.74 7.797c-1.594.484-2.75.844-2.88.896c-.901.286-.927.313-1.031 1.161c-.099.615-2.438 18.75-2.438 18.75L20.01 32z"/>
|
<path fill="currentColor" d="m20.448 31.974l9.625-2.083s-3.474-23.484-3.5-23.641s-.156-.255-.281-.255c-.13 0-2.573-.182-2.573-.182s-1.703-1.698-1.922-1.88a.4.4 0 0 0-.161-.099l-1.219 28.141zm-4.833-16.901s-1.083-.563-2.365-.563c-1.932 0-2.005 1.203-2.005 1.521c0 1.641 4.318 2.286 4.318 6.172c0 3.057-1.922 5.01-4.542 5.01c-3.141 0-4.719-1.953-4.719-1.953l.859-2.781s1.661 1.422 3.042 1.422c.901 0 1.302-.724 1.302-1.245c0-2.156-3.542-2.255-3.542-5.807c-.047-2.984 2.094-5.891 6.438-5.891c1.677 0 2.5.479 2.5.479l-1.26 3.625zm-.719-13.969c.177 0 .359.052.536.182c-1.313.62-2.75 2.188-3.344 5.323a76 76 0 0 1-2.516.771c.688-2.38 2.359-6.26 5.323-6.26zm1.646 3.932v.182c-1.005.307-2.115.646-3.193.979c.62-2.37 1.776-3.526 2.781-3.958c.255.667.411 1.568.411 2.797zm.718-2.973c.922.094 1.521 1.151 1.901 2.339c-.464.151-.979.307-1.542.484v-.333c0-1.005-.13-1.828-.359-2.495zm3.99 1.718c-.031 0-.083.026-.104.026c-.026 0-.385.099-.953.281C19.63 2.442 18.625.927 16.849.927h-.156C16.183.281 15.558 0 15.021 0c-4.141 0-6.12 5.172-6.74 7.797c-1.594.484-2.75.844-2.88.896c-.901.286-.927.313-1.031 1.161c-.099.615-2.438 18.75-2.438 18.75L20.01 32z"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
</svg>
|
<symbol id="icon-slack" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M6.527 14.514A1.973 1.973 0 0 1 4.56 16.48a1.973 1.973 0 0 1-1.968-1.967c0-1.083.885-1.968 1.968-1.968h1.967zm.992 0c0-1.083.885-1.968 1.968-1.968s1.967.885 1.967 1.968v4.927a1.973 1.973 0 0 1-1.967 1.968a1.973 1.973 0 0 1-1.968-1.968zm1.968-7.987A1.973 1.973 0 0 1 7.519 4.56c0-1.083.885-1.967 1.968-1.967s1.967.884 1.967 1.967v1.968zm0 .992c1.083 0 1.967.884 1.967 1.967a1.973 1.973 0 0 1-1.967 1.968H4.56a1.973 1.973 0 0 1-1.968-1.968c0-1.083.885-1.967 1.968-1.967zm7.986 1.967c0-1.083.885-1.967 1.968-1.967s1.968.884 1.968 1.967a1.973 1.973 0 0 1-1.968 1.968h-1.968zm-.991 0a1.973 1.973 0 0 1-1.968 1.968a1.973 1.973 0 0 1-1.968-1.968V4.56c0-1.083.885-1.967 1.968-1.967s1.968.884 1.968 1.967zm-1.968 7.987c1.083 0 1.968.885 1.968 1.968a1.973 1.973 0 0 1-1.968 1.968a1.973 1.973 0 0 1-1.968-1.968v-1.968zm0-.992a1.973 1.973 0 0 1-1.968-1.967c0-1.083.885-1.968 1.968-1.968h4.927c1.083 0 1.968.885 1.968 1.968a1.973 1.973 0 0 1-1.968 1.967z"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
@@ -2,7 +2,7 @@
|
|||||||
<hr/>
|
<hr/>
|
||||||
<div class="form-actions">
|
<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>
|
<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>
|
</div>
|
||||||
</section>
|
</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
|
# Alfred
|
||||||
# Add here as you use it for more features
|
# Add here as you use it for more features
|
||||||
# Used for Round Robin, Conversation Emails & Online Presence
|
# 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?
|
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]
|
resource '*', headers: :any, methods: :any, expose: %w[access-token client uid expiry]
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ class Rack::Attack
|
|||||||
# Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
# Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new
|
||||||
|
|
||||||
# https://github.com/rack/rack-attack/issues/102
|
# 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
|
class Request < ::Rack::Request
|
||||||
# You many need to specify a method to fetch the correct remote IP address
|
# You many need to specify a method to fetch the correct remote IP address
|
||||||
|
|||||||
@@ -235,6 +235,14 @@
|
|||||||
description: Used to notify Chatwoot about account abuses, potential threads (Should be a Discord Webhook URL)
|
description: Used to notify Chatwoot about account abuses, potential threads (Should be a Discord Webhook URL)
|
||||||
# ------- End of Chatwoot Internal Config for Self Hosted ----#
|
# ------- End of Chatwoot Internal Config for Self Hosted ----#
|
||||||
|
|
||||||
|
# ------- Compliance Related Config ----#
|
||||||
|
- name: CHATWOOT_INSTANCE_ADMIN_EMAIL
|
||||||
|
display_title: 'Instance Admin Email'
|
||||||
|
value:
|
||||||
|
description: 'The email of the instance administrator to receive compliance-related notifications'
|
||||||
|
locked: false
|
||||||
|
# ------- End of Compliance Related Config ----#
|
||||||
|
|
||||||
## ------ Configs added for enterprise clients ------ ##
|
## ------ Configs added for enterprise clients ------ ##
|
||||||
- name: API_CHANNEL_NAME
|
- name: API_CHANNEL_NAME
|
||||||
value:
|
value:
|
||||||
@@ -280,6 +288,20 @@
|
|||||||
type: secret
|
type: secret
|
||||||
## ------ End of Configs added for Linear ------ ##
|
## ------ End of Configs added for Linear ------ ##
|
||||||
|
|
||||||
|
## ------ Configs added for Slack ------ ##
|
||||||
|
- name: SLACK_CLIENT_ID
|
||||||
|
display_title: 'Slack Client ID'
|
||||||
|
value:
|
||||||
|
locked: false
|
||||||
|
description: 'Slack client ID'
|
||||||
|
- name: SLACK_CLIENT_SECRET
|
||||||
|
display_title: 'Slack Client Secret'
|
||||||
|
value:
|
||||||
|
locked: false
|
||||||
|
description: 'Slack client secret'
|
||||||
|
type: secret
|
||||||
|
## ------ End of Configs added for Slack ------ ##
|
||||||
|
|
||||||
# ------- Shopify Integration Config ------- #
|
# ------- Shopify Integration Config ------- #
|
||||||
- name: SHOPIFY_CLIENT_ID
|
- name: SHOPIFY_CLIENT_ID
|
||||||
display_title: 'Shopify Client ID'
|
display_title: 'Shopify Client ID'
|
||||||
|
|||||||
@@ -213,33 +213,41 @@ en:
|
|||||||
online:
|
online:
|
||||||
delete: '%{contact_name} is Online, please try again later'
|
delete: '%{contact_name} is Online, please try again later'
|
||||||
integration_apps:
|
integration_apps:
|
||||||
|
# Note: webhooks and dashboard_apps don't need short_description as they use different modal components
|
||||||
dashboard_apps:
|
dashboard_apps:
|
||||||
name: 'Dashboard Apps'
|
name: 'Dashboard Apps'
|
||||||
description: 'Dashboard Apps allow you to create and embed applications that display user information, orders, or payment history, providing more context to your customer support agents.'
|
description: 'Dashboard Apps allow you to create and embed applications that display user information, orders, or payment history, providing more context to your customer support agents.'
|
||||||
dyte:
|
dyte:
|
||||||
name: 'Dyte'
|
name: 'Dyte'
|
||||||
|
short_description: 'Start video/voice calls with customers directly from Chatwoot.'
|
||||||
description: 'Dyte is a product that integrates audio and video functionalities into your application. With this integration, your agents can start video/voice calls with your customers directly from Chatwoot.'
|
description: 'Dyte is a product that integrates audio and video functionalities into your application. With this integration, your agents can start video/voice calls with your customers directly from Chatwoot.'
|
||||||
meeting_name: '%{agent_name} has started a meeting'
|
meeting_name: '%{agent_name} has started a meeting'
|
||||||
slack:
|
slack:
|
||||||
name: 'Slack'
|
name: 'Slack'
|
||||||
|
short_description: 'Receive notifications and respond to conversations directly in Slack.'
|
||||||
description: "Integrate Chatwoot with Slack to keep your team in sync. This integration allows you to receive notifications for new conversations and respond to them directly within Slack's interface."
|
description: "Integrate Chatwoot with Slack to keep your team in sync. This integration allows you to receive notifications for new conversations and respond to them directly within Slack's interface."
|
||||||
webhooks:
|
webhooks:
|
||||||
name: 'Webhooks'
|
name: 'Webhooks'
|
||||||
description: 'Webhook events provide real-time updates about activities in your Chatwoot account. You can subscribe to your preferred events, and Chatwoot will send you HTTP callbacks with the updates.'
|
description: 'Webhook events provide real-time updates about activities in your Chatwoot account. You can subscribe to your preferred events, and Chatwoot will send you HTTP callbacks with the updates.'
|
||||||
dialogflow:
|
dialogflow:
|
||||||
name: 'Dialogflow'
|
name: 'Dialogflow'
|
||||||
|
short_description: 'Build chatbots to handle initial queries before transferring to agents.'
|
||||||
description: 'Build chatbots with Dialogflow and easily integrate them into your inbox. These bots can handle initial queries before transferring them to a customer service agent.'
|
description: 'Build chatbots with Dialogflow and easily integrate them into your inbox. These bots can handle initial queries before transferring them to a customer service agent.'
|
||||||
google_translate:
|
google_translate:
|
||||||
name: 'Google Translate'
|
name: 'Google Translate'
|
||||||
|
short_description: 'Automatically translate customer messages for agents.'
|
||||||
description: "Integrate Google Translate to help agents easily translate customer messages. This integration automatically detects the language and converts it to the agent's or admin's preferred language."
|
description: "Integrate Google Translate to help agents easily translate customer messages. This integration automatically detects the language and converts it to the agent's or admin's preferred language."
|
||||||
openai:
|
openai:
|
||||||
name: 'OpenAI'
|
name: 'OpenAI'
|
||||||
|
short_description: 'AI-powered reply suggestions, summarization, and message enhancement.'
|
||||||
description: 'Leverage the power of large language models from OpenAI with the features such as reply suggestions, summarization, message rephrasing, spell-checking, and label classification.'
|
description: 'Leverage the power of large language models from OpenAI with the features such as reply suggestions, summarization, message rephrasing, spell-checking, and label classification.'
|
||||||
linear:
|
linear:
|
||||||
name: 'Linear'
|
name: 'Linear'
|
||||||
|
short_description: 'Create and link Linear issues directly from conversations.'
|
||||||
description: 'Create issues in Linear directly from your conversation window. Alternatively, link existing Linear issues for a more streamlined and efficient issue tracking process.'
|
description: 'Create issues in Linear directly from your conversation window. Alternatively, link existing Linear issues for a more streamlined and efficient issue tracking process.'
|
||||||
shopify:
|
shopify:
|
||||||
name: 'Shopify'
|
name: 'Shopify'
|
||||||
|
short_description: 'Access order details and customer data from your Shopify store.'
|
||||||
description: 'Connect your Shopify store to access order details, customer information, and product data directly within your conversations and helps your support team provide faster, more contextual assistance to your customers.'
|
description: 'Connect your Shopify store to access order details, customer information, and product data directly within your conversations and helps your support team provide faster, more contextual assistance to your customers.'
|
||||||
leadsquared:
|
leadsquared:
|
||||||
name: 'LeadSquared'
|
name: 'LeadSquared'
|
||||||
|
|||||||
+3
-2
@@ -60,8 +60,8 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
resources :assistant_responses
|
resources :assistant_responses
|
||||||
resources :bulk_actions, only: [:create]
|
resources :bulk_actions, only: [:create]
|
||||||
resources :copilot_threads, only: [:index] do
|
resources :copilot_threads, only: [:index, :create] do
|
||||||
resources :copilot_messages, only: [:index]
|
resources :copilot_messages, only: [:index, :create]
|
||||||
end
|
end
|
||||||
resources :documents, only: [:index, :show, :create, :destroy]
|
resources :documents, only: [:index, :show, :create, :destroy]
|
||||||
end
|
end
|
||||||
@@ -396,6 +396,7 @@ Rails.application.routes.draw do
|
|||||||
resources :users, only: [:create, :show, :update, :destroy] do
|
resources :users, only: [:create, :show, :update, :destroy] do
|
||||||
member do
|
member do
|
||||||
get :login
|
get :login
|
||||||
|
post :token
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
|
resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
|
||||||
|
|||||||
@@ -39,3 +39,10 @@ process_stale_contacts_job:
|
|||||||
cron: '30 04 * * *'
|
cron: '30 04 * * *'
|
||||||
class: 'Internal::ProcessStaleContactsJob'
|
class: 'Internal::ProcessStaleContactsJob'
|
||||||
queue: housekeeping
|
queue: housekeeping
|
||||||
|
|
||||||
|
# executed daily at 0100 UTC
|
||||||
|
# to delete accounts marked for deletion
|
||||||
|
delete_accounts_job:
|
||||||
|
cron: '0 1 * * *'
|
||||||
|
class: 'Internal::DeleteAccountsJob'
|
||||||
|
queue: scheduled_jobs
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
class RemoveUuidFromCopilotThreads < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
remove_column :copilot_threads, :uuid, :string
|
||||||
|
|
||||||
|
add_column :copilot_threads, :assistant_id, :integer
|
||||||
|
add_index :copilot_threads, :assistant_id
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class RemoveUserIdFromCopilotMessages < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
remove_reference :copilot_messages, :user, index: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class ChangeMessageTypeToIntegerInCopilotMessages < ActiveRecord::Migration[7.1]
|
||||||
|
def up
|
||||||
|
remove_column :copilot_messages, :message_type
|
||||||
|
add_column :copilot_messages, :message_type, :integer, default: 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :copilot_messages, :message_type
|
||||||
|
add_column :copilot_messages, :message_type, :string, default: 'user'
|
||||||
|
end
|
||||||
|
end
|
||||||
+4
-6
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2025_05_14_045638) do
|
ActiveRecord::Schema[7.1].define(version: 2025_05_23_031839) do
|
||||||
# These extensions should be enabled to support this database
|
# These extensions should be enabled to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
enable_extension "pg_trgm"
|
enable_extension "pg_trgm"
|
||||||
@@ -577,27 +577,25 @@ ActiveRecord::Schema[7.0].define(version: 2025_05_14_045638) do
|
|||||||
|
|
||||||
create_table "copilot_messages", force: :cascade do |t|
|
create_table "copilot_messages", force: :cascade do |t|
|
||||||
t.bigint "copilot_thread_id", null: false
|
t.bigint "copilot_thread_id", null: false
|
||||||
t.bigint "user_id", null: false
|
|
||||||
t.bigint "account_id", null: false
|
t.bigint "account_id", null: false
|
||||||
t.string "message_type", null: false
|
|
||||||
t.jsonb "message", default: {}, null: false
|
t.jsonb "message", default: {}, null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.integer "message_type", default: 0
|
||||||
t.index ["account_id"], name: "index_copilot_messages_on_account_id"
|
t.index ["account_id"], name: "index_copilot_messages_on_account_id"
|
||||||
t.index ["copilot_thread_id"], name: "index_copilot_messages_on_copilot_thread_id"
|
t.index ["copilot_thread_id"], name: "index_copilot_messages_on_copilot_thread_id"
|
||||||
t.index ["user_id"], name: "index_copilot_messages_on_user_id"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "copilot_threads", force: :cascade do |t|
|
create_table "copilot_threads", force: :cascade do |t|
|
||||||
t.string "title", null: false
|
t.string "title", null: false
|
||||||
t.bigint "user_id", null: false
|
t.bigint "user_id", null: false
|
||||||
t.bigint "account_id", null: false
|
t.bigint "account_id", null: false
|
||||||
t.uuid "uuid", default: -> { "gen_random_uuid()" }, null: false
|
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.integer "assistant_id"
|
||||||
t.index ["account_id"], name: "index_copilot_threads_on_account_id"
|
t.index ["account_id"], name: "index_copilot_threads_on_account_id"
|
||||||
|
t.index ["assistant_id"], name: "index_copilot_threads_on_assistant_id"
|
||||||
t.index ["user_id"], name: "index_copilot_threads_on_user_id"
|
t.index ["user_id"], name: "index_copilot_threads_on_user_id"
|
||||||
t.index ["uuid"], name: "index_copilot_threads_on_uuid", unique: true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "csat_survey_responses", force: :cascade do |t|
|
create_table "csat_survey_responses", force: :cascade do |t|
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ KillMode=mixed
|
|||||||
StandardInput=null
|
StandardInput=null
|
||||||
SyslogIdentifier=%p
|
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="PORT=3000"
|
||||||
Environment="RAILS_ENV=production"
|
Environment="RAILS_ENV=production"
|
||||||
Environment="NODE_ENV=production"
|
Environment="NODE_ENV=production"
|
||||||
Environment="RAILS_LOG_TO_STDOUT=true"
|
Environment="RAILS_LOG_TO_STDOUT=true"
|
||||||
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.3.3"
|
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.4.4"
|
||||||
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3:/home/chatwoot/.rvm/gems/ruby-3.3.3@global"
|
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
|
MemorySwapMax=0
|
||||||
OOMPolicy=stop
|
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="PORT=3000"
|
||||||
Environment="RAILS_ENV=production"
|
Environment="RAILS_ENV=production"
|
||||||
Environment="NODE_ENV=production"
|
Environment="NODE_ENV=production"
|
||||||
Environment="RAILS_LOG_TO_STDOUT=true"
|
Environment="RAILS_LOG_TO_STDOUT=true"
|
||||||
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.3.3"
|
Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-3.4.4"
|
||||||
Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-3.3.3:/home/chatwoot/.rvm/gems/ruby-3.3.3@global"
|
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
|
sudo -i -u chatwoot << EOF
|
||||||
rvm --version
|
rvm --version
|
||||||
rvm autolibs disable
|
rvm autolibs disable
|
||||||
rvm install "ruby-3.3.3"
|
rvm install "ruby-3.4.4"
|
||||||
rvm use 3.3.3 --default
|
rvm use 3.4.4 --default
|
||||||
|
|
||||||
git clone https://github.com/chatwoot/chatwoot.git
|
git clone https://github.com/chatwoot/chatwoot.git
|
||||||
cd chatwoot
|
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
|
# pre-build stage
|
||||||
FROM node:23-alpine as node
|
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 NODE_VERSION="23.7.0"
|
||||||
ARG PNPM_VERSION="10.2.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
|
RUN git rev-parse HEAD > /app/.git_sha
|
||||||
|
|
||||||
# Remove unnecessary files
|
# Remove unnecessary files
|
||||||
RUN rm -rf /gems/ruby/3.3.0/cache/*.gem \
|
RUN rm -rf /gems/ruby/3.4.0/cache/*.gem \
|
||||||
&& find /gems/ruby/3.3.0/gems/ \( -name "*.c" -o -name "*.o" \) -delete \
|
&& find /gems/ruby/3.4.0/gems/ \( -name "*.c" -o -name "*.o" \) -delete \
|
||||||
&& rm -rf .git \
|
&& rm -rf .git \
|
||||||
&& rm .gitignore
|
&& rm .gitignore
|
||||||
|
|
||||||
# final build stage
|
# final build stage
|
||||||
FROM ruby:3.3.3-alpine3.19
|
FROM ruby:3.4.4-alpine3.21
|
||||||
|
|
||||||
ARG NODE_VERSION="23.7.0"
|
ARG NODE_VERSION="23.7.0"
|
||||||
ARG PNPM_VERSION="10.2.0"
|
ARG PNPM_VERSION="10.2.0"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
|
|||||||
config: [
|
config: [
|
||||||
:product_name, :feature_faq, :feature_memory,
|
:product_name, :feature_faq, :feature_memory,
|
||||||
:welcome_message, :handoff_message, :resolution_message,
|
:welcome_message, :handoff_message, :resolution_message,
|
||||||
:instructions
|
:instructions, :temperature
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user