diff --git a/.circleci/config.yml b/.circleci/config.yml
index a9e0bd450..fbb4fea90 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -19,6 +19,7 @@ defaults: &defaults
- COVERAGE: true
- LOG_LEVEL: warn
parallelism: 4
+ resource_class: large
jobs:
build:
@@ -122,9 +123,11 @@ jobs:
mkdir -p coverage
~/tmp/cc-test-reporter before-build
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
- bundle exec rspec --profile 10 \
- --out test-results/rspec/rspec.xml \
+ bundle exec rspec --format progress \
+ --format RspecJunitFormatter \
+ --out ~/tmp/test-results/rspec.xml \
-- ${TESTFILES}
+ no_output_timeout: 30m
- run:
name: Code Climate Test Coverage
command: |
@@ -137,7 +140,7 @@ jobs:
~/tmp/cc-test-reporter before-build
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings)
yarn test:coverage --profile 10 \
- --out test-results/frontend_specs/rspec.xml \
+ --out ~/tmp/test-results/yarn.xml \
-- ${TESTFILES}
- run:
name: Code Climate Test Coverage
diff --git a/.editorconfig b/.editorconfig
index 7203adb09..2a5fe28cf 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -7,8 +7,8 @@ end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
-indent_style = spaces
+indent_style = space
tab_width = 2
-[{*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}]
+[*.{rb,erb,js,coffee,json,yml,css,scss,sh,markdown,md,html}]
indent_size = 2
diff --git a/.github/workflows/publish_foss_docker.yml b/.github/workflows/publish_foss_docker.yml
index 37f0f3e6e..2ddaba7e5 100644
--- a/.github/workflows/publish_foss_docker.yml
+++ b/.github/workflows/publish_foss_docker.yml
@@ -58,5 +58,6 @@ jobs:
with:
context: .
file: docker/Dockerfile
+ platforms: linux/amd64
push: true
tags: ${{ env.DOCKER_TAG }}
diff --git a/.gitignore b/.gitignore
index e4b14d2f5..11a8c50e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,9 +39,6 @@ public/packs*
*.un~
.jest-cache
-#VS Code files
-.vscode
-
# ignore jetbrains IDE files
.idea
@@ -62,4 +59,4 @@ package-lock.json
test/cypress/videos/*
/config/master.key
-/config/*.enc
\ No newline at end of file
+/config/*.enc
diff --git a/.rubocop.yml b/.rubocop.yml
index dafd9a620..3665ad2e3 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -16,6 +16,7 @@ Metrics/ClassLength:
- 'app/models/message.rb'
- 'app/builders/messages/facebook/message_builder.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
+ - 'app/controllers/api/v1/accounts/conversations_controller.rb'
- 'app/listeners/action_cable_listener.rb'
- 'app/models/conversation.rb'
RSpec/ExampleLength:
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 000000000..254e696a4
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,32 @@
+{
+ "recommendations": [
+ // Spell check
+ "streetsidesoftware.code-spell-checker",
+ // Better Comments
+ "aaron-bond.better-comments",
+ // Rails Test Runner
+ "davidpallinder.rails-test-runner",
+ // Eslint
+ "dbaeumer.vscode-eslint",
+ // Auto Close Tag
+ "formulahendry.auto-close-tag",
+ // Auto Rename Tag
+ "formulahendry.auto-rename-tag",
+ // Hight light colors
+ "naumovs.color-highlight",
+ // GitLens
+ "eamodio.gitlens",
+ // Ruby
+ "rebornix.ruby",
+ // Vue
+ "octref.vetur",
+ // Prettier
+ "esbenp.prettier-vscode",
+ // Dot Env
+ "mikestead.dotenv",
+ // HTML CSS Support
+ "ecmel.vscode-html-css",
+ // Tailwind CSS Intellisense
+ "bradlc.vscode-tailwindcss",
+ ]
+}
diff --git a/Gemfile b/Gemfile
index ca6c7856d..d47b5e449 100644
--- a/Gemfile
+++ b/Gemfile
@@ -174,6 +174,7 @@ group :development, :test do
gem 'listen'
gem 'mock_redis'
gem 'pry-rails'
+ gem 'rspec_junit_formatter'
gem 'rspec-rails', '~> 5.0.0'
gem 'rubocop', require: false
gem 'rubocop-performance', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 5cf4f020d..5b14d5b5e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -286,9 +286,9 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
- google-protobuf (3.21.2)
- google-protobuf (3.21.2-x86_64-darwin)
- google-protobuf (3.21.2-x86_64-linux)
+ google-protobuf (3.21.7)
+ google-protobuf (3.21.7-x86_64-darwin)
+ google-protobuf (3.21.7-x86_64-linux)
googleapis-common-protos (1.3.12)
google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.2)
@@ -536,6 +536,8 @@ GEM
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.11.0)
+ rspec_junit_formatter (0.6.0)
+ rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.31.2)
json (~> 2.3)
parallel (~> 1.10)
@@ -769,6 +771,7 @@ DEPENDENCIES
responders
rest-client
rspec-rails (~> 5.0.0)
+ rspec_junit_formatter
rubocop
rubocop-performance
rubocop-rails
@@ -805,4 +808,4 @@ RUBY VERSION
ruby 3.0.4p208
BUNDLED WITH
- 2.3.17
+ 2.3.18
diff --git a/app/builders/contact_inbox_builder.rb b/app/builders/contact_inbox_builder.rb
index e7ae8b0aa..8fcd2b158 100644
--- a/app/builders/contact_inbox_builder.rb
+++ b/app/builders/contact_inbox_builder.rb
@@ -1,13 +1,12 @@
+# This Builder will create a contact inbox with specified attributes. If the contact inbox already exists, it will be returned.
+# For Specific Channels like whatsapp, email etc . it smartly generated appropriate the source id when none is provided.
+
class ContactInboxBuilder
- pattr_initialize [:contact_id!, :inbox_id!, :source_id]
+ pattr_initialize [:contact, :inbox, :source_id, { hmac_verified: false }]
def perform
- @contact = Contact.find(contact_id)
- @inbox = @contact.account.inboxes.find(inbox_id)
- return unless ['Channel::TwilioSms', 'Channel::Sms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
-
- source_id = @source_id || generate_source_id
- create_contact_inbox(source_id) if source_id.present?
+ @source_id ||= generate_source_id
+ create_contact_inbox if source_id.present?
end
private
@@ -19,23 +18,37 @@ class ContactInboxBuilder
when 'Channel::Whatsapp'
wa_source_id
when 'Channel::Email'
- @contact.email
+ email_source_id
when 'Channel::Sms'
- @contact.phone_number
- when 'Channel::Api'
+ phone_source_id
+ when 'Channel::Api', 'Channel::WebWidget'
SecureRandom.uuid
+ else
+ raise "Unsupported operation for this channel: #{@inbox.channel_type}"
end
end
+ def email_source_id
+ raise ActionController::ParameterMissing, 'contact email' unless @contact.email
+
+ @contact.email
+ end
+
+ def phone_source_id
+ raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
+
+ @contact.phone_number
+ end
+
def wa_source_id
- return unless @contact.phone_number
+ raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
# whatsapp doesn't want the + in e164 format
- "#{@contact.phone_number}.delete('+')"
+ @contact.phone_number.delete('+').to_s
end
def twilio_source_id
- return unless @contact.phone_number
+ raise ActionController::ParameterMissing, 'contact phone number' unless @contact.phone_number
case @inbox.channel.medium
when 'sms'
@@ -45,11 +58,11 @@ class ContactInboxBuilder
end
end
- def create_contact_inbox(source_id)
- ::ContactInbox.find_or_create_by!(
+ def create_contact_inbox
+ ::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
contact_id: @contact.id,
inbox_id: @inbox.id,
- source_id: source_id
+ source_id: @source_id
)
end
end
diff --git a/app/builders/contact_builder.rb b/app/builders/contact_inbox_with_contact_builder.rb
similarity index 51%
rename from app/builders/contact_builder.rb
rename to app/builders/contact_inbox_with_contact_builder.rb
index 938072643..d97f64cfe 100644
--- a/app/builders/contact_builder.rb
+++ b/app/builders/contact_inbox_with_contact_builder.rb
@@ -1,25 +1,47 @@
-class ContactBuilder
- pattr_initialize [:source_id!, :inbox!, :contact_attributes!, :hmac_verified]
+# This Builder will create a contact and contact inbox with specified attributes.
+# If an existing identified contact exisits, it will be returned.
+# for contact inbox logic it uses the contact inbox builder
+
+class ContactInboxWithContactBuilder
+ pattr_initialize [:inbox!, :contact_attributes!, :source_id, :hmac_verified]
def perform
- contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
- return contact_inbox if contact_inbox
+ find_or_create_contact_and_contact_inbox
+ # in case of race conditions where contact is created by another thread
+ # we will try to find the contact and create a contact inbox
+ rescue ActiveRecord::RecordNotUnique
+ find_or_create_contact_and_contact_inbox
+ end
- build_contact_inbox
+ def find_or_create_contact_and_contact_inbox
+ @contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) if source_id.present?
+ return @contact_inbox if @contact_inbox
+
+ ActiveRecord::Base.transaction(requires_new: true) do
+ build_contact_with_contact_inbox
+ update_contact_avatar(@contact) unless @contact.avatar.attached?
+ @contact_inbox
+ end
end
private
+ def build_contact_with_contact_inbox
+ @contact = find_contact || create_contact
+ @contact_inbox = create_contact_inbox
+ end
+
def account
@account ||= inbox.account
end
- def create_contact_inbox(contact)
- ::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
- contact_id: contact.id,
- inbox_id: inbox.id,
- source_id: source_id
- )
+ def create_contact_inbox
+ ContactInboxBuilder.new(
+ contact: @contact,
+ inbox: @inbox,
+ source_id: @source_id,
+ hmac_verified: hmac_verified
+ ).perform
end
def update_contact_avatar(contact)
@@ -61,16 +83,4 @@ class ContactBuilder
account.contacts.find_by(phone_number: phone_number)
end
-
- def build_contact_inbox
- ActiveRecord::Base.transaction do
- contact = find_contact || create_contact
- contact_inbox = create_contact_inbox(contact)
- update_contact_avatar(contact)
- contact_inbox
- rescue StandardError => e
- Rails.logger.error e
- raise e
- end
- end
end
diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb
index 9f670602a..42d567e54 100644
--- a/app/builders/messages/facebook/message_builder.rb
+++ b/app/builders/messages/facebook/message_builder.rb
@@ -22,10 +22,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
return if @inbox.channel.reauthorization_required?
ActiveRecord::Base.transaction do
- build_contact
+ build_contact_inbox
build_message
end
- ensure_contact_avatar
rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error!
rescue StandardError => e
@@ -35,15 +34,12 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
private
- def contact
- @contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
- end
-
- def build_contact
- return if contact.present?
-
- @contact = Contact.create!(contact_params.except(:remote_avatar_url))
- @contact_inbox = ContactInbox.find_or_create_by!(contact: contact, inbox: @inbox, source_id: @sender_id)
+ def build_contact_inbox
+ @contact_inbox = ::ContactInboxWithContactBuilder.new(
+ source_id: @sender_id,
+ inbox: @inbox,
+ contact_attributes: contact_params
+ ).perform
end
def build_message
@@ -54,19 +50,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end
end
- def ensure_contact_avatar
- return if contact_params[:remote_avatar_url].blank?
- return if @contact.avatar.attached?
-
- Avatar::AvatarFromUrlJob.perform_later(@contact, contact_params[:remote_avatar_url])
- end
-
def conversation
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
end
def build_conversation
- @contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
Conversation.create!(conversation_params.merge(
contact_inbox_id: @contact_inbox.id
))
@@ -94,7 +82,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
- contact_id: contact.id
+ contact_id: @contact_inbox.contact_id
}
end
@@ -105,7 +93,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
message_type: @message_type,
content: response.content,
source_id: response.identifier,
- sender: @outgoing_echo ? nil : contact
+ sender: @outgoing_echo ? nil : @contact_inbox.contact
}
end
@@ -113,7 +101,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
{
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
account_id: @inbox.account_id,
- remote_avatar_url: result['profile_pic'] || ''
+ avatar_url: result['profile_pic']
}
end
diff --git a/app/controllers/api/v1/accounts/articles_controller.rb b/app/controllers/api/v1/accounts/articles_controller.rb
index a2aab527c..d650b19d9 100644
--- a/app/controllers/api/v1/accounts/articles_controller.rb
+++ b/app/controllers/api/v1/accounts/articles_controller.rb
@@ -42,8 +42,8 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
def article_params
params.require(:article).permit(
- :title, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title, :description,
- { tags: [] }]
+ :title, :slug, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title, :description,
+ { tags: [] }]
)
end
diff --git a/app/controllers/api/v1/accounts/contacts/contact_inboxes_controller.rb b/app/controllers/api/v1/accounts/contacts/contact_inboxes_controller.rb
index fdcdcaf9e..b4287ae08 100644
--- a/app/controllers/api/v1/accounts/contacts/contact_inboxes_controller.rb
+++ b/app/controllers/api/v1/accounts/contacts/contact_inboxes_controller.rb
@@ -2,8 +2,11 @@ class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts:
before_action :ensure_inbox, only: [:create]
def create
- source_id = params[:source_id] || SecureRandom.uuid
- @contact_inbox = ContactInbox.create!(contact: @contact, inbox: @inbox, source_id: source_id)
+ @contact_inbox = ContactInboxBuilder.new(
+ contact: @contact,
+ inbox: @inbox,
+ source_id: params[:source_id]
+ ).perform
end
private
diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb
index 1c56e9c04..b86b973df 100644
--- a/app/controllers/api/v1/accounts/contacts_controller.rb
+++ b/app/controllers/api/v1/accounts/contacts_controller.rb
@@ -134,8 +134,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
return if params[:inbox_id].blank?
inbox = Current.account.inboxes.find(params[:inbox_id])
- source_id = params[:source_id] || SecureRandom.uuid
- ContactInbox.create!(contact: @contact, inbox: inbox, source_id: source_id)
+ ContactInboxBuilder.new(
+ contact: @contact,
+ inbox: inbox,
+ source_id: params[:source_id]
+ ).perform
end
def permitted_params
diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb
index 0515eabca..8734a3dd4 100644
--- a/app/controllers/api/v1/accounts/conversations_controller.rb
+++ b/app/controllers/api/v1/accounts/conversations_controller.rb
@@ -3,7 +3,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
include DateRangeHelper
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
- before_action :contact_inbox, only: [:create]
+ before_action :inbox, :contact, :contact_inbox, only: [:create]
def index
result = conversation_finder.perform
@@ -109,22 +109,35 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
authorize @conversation.inbox, :show?
end
+ def inbox
+ return if params[:inbox_id].blank?
+
+ @inbox = Current.account.inboxes.find(params[:inbox_id])
+ authorize @inbox, :show?
+ end
+
+ def contact
+ return if params[:contact_id].blank?
+
+ @contact = Current.account.contacts.find(params[:contact_id])
+ end
+
def contact_inbox
@contact_inbox = build_contact_inbox
+ # fallback for the old case where we do look up only using source id
+ # In future we need to change this and make sure we do look up on combination of inbox_id and source_id
+ # and deprecate the support of passing only source_id as the param
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
authorize @contact_inbox.inbox, :show?
end
def build_contact_inbox
- return if params[:contact_id].blank? || params[:inbox_id].blank?
-
- inbox = Current.account.inboxes.find(params[:inbox_id])
- authorize inbox, :show?
+ return if @inbox.blank? || @contact.blank?
ContactInboxBuilder.new(
- contact_id: params[:contact_id],
- inbox_id: inbox.id,
+ contact: @contact,
+ inbox: @inbox,
source_id: params[:source_id]
).perform
end
diff --git a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb
index 7b5c51d6e..9495bbfa8 100644
--- a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb
+++ b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb
@@ -22,7 +22,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
def download
response.headers['Content-Type'] = 'text/csv'
response.headers['Content-Disposition'] = 'attachment; filename=csat_report.csv'
- render layout: false, template: 'api/v1/accounts/csat_survey_responses/download.csv.erb', format: 'csv'
+ render layout: false, template: 'api/v1/accounts/csat_survey_responses/download', formats: [:csv]
end
private
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 2f18fbac2..507e00e64 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -24,7 +24,7 @@ class Api::V1::AccountsController < Api::BaseController
).perform
if @user
send_auth_headers(@user)
- render 'api/v1/accounts/create.json', locals: { resource: @user }
+ render 'api/v1/accounts/create', format: :json, locals: { resource: @user }
else
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
end
@@ -32,7 +32,7 @@ class Api::V1::AccountsController < Api::BaseController
def show
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
- render 'api/v1/accounts/show.json'
+ render 'api/v1/accounts/show', format: :json
end
def update
diff --git a/app/controllers/api/v1/notification_subscriptions_controller.rb b/app/controllers/api/v1/notification_subscriptions_controller.rb
index a01c2ca03..1a797a74d 100644
--- a/app/controllers/api/v1/notification_subscriptions_controller.rb
+++ b/app/controllers/api/v1/notification_subscriptions_controller.rb
@@ -9,7 +9,7 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
def destroy
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
- notification_subscription.destroy!
+ notification_subscription.destroy! if notification_subscription.present?
head :ok
end
diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb
index fb9c3b62c..20b8e7ae8 100644
--- a/app/controllers/api/v1/profiles_controller.rb
+++ b/app/controllers/api/v1/profiles_controller.rb
@@ -22,6 +22,11 @@ class Api::V1::ProfilesController < Api::BaseController
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
end
+ def set_active_account
+ @user.account_users.find_by(account_id: profile_params[:account_id]).update(active_at: Time.now.utc)
+ head :ok
+ end
+
private
def set_user
@@ -39,6 +44,7 @@ class Api::V1::ProfilesController < Api::BaseController
:display_name,
:avatar,
:message_signature,
+ :account_id,
ui_settings: {}
)
end
diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb
index bbc4dde7b..dedeb17bf 100644
--- a/app/controllers/api/v2/accounts/reports_controller.rb
+++ b/app/controllers/api/v2/accounts/reports_controller.rb
@@ -14,22 +14,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
def agents
@report_data = generate_agents_report
- generate_csv('agents_report', 'api/v2/accounts/reports/agents.csv.erb')
+ generate_csv('agents_report', 'api/v2/accounts/reports/agents')
end
def inboxes
@report_data = generate_inboxes_report
- generate_csv('inboxes_report', 'api/v2/accounts/reports/inboxes.csv.erb')
+ generate_csv('inboxes_report', 'api/v2/accounts/reports/inboxes')
end
def labels
@report_data = generate_labels_report
- generate_csv('labels_report', 'api/v2/accounts/reports/labels.csv.erb')
+ generate_csv('labels_report', 'api/v2/accounts/reports/labels')
end
def teams
@report_data = generate_teams_report
- generate_csv('teams_report', 'api/v2/accounts/reports/teams.csv.erb')
+ generate_csv('teams_report', 'api/v2/accounts/reports/teams')
end
def conversations
@@ -43,7 +43,7 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
def generate_csv(filename, template)
response.headers['Content-Type'] = 'text/csv'
response.headers['Content-Disposition'] = "attachment; filename=#{filename}.csv"
- render layout: false, template: template, format: 'csv'
+ render layout: false, template: template, formats: [:csv]
end
def check_authorization
diff --git a/app/controllers/concerns/request_exception_handler.rb b/app/controllers/concerns/request_exception_handler.rb
index 6e9ed04cc..2f53fdc2b 100644
--- a/app/controllers/concerns/request_exception_handler.rb
+++ b/app/controllers/concerns/request_exception_handler.rb
@@ -13,6 +13,8 @@ module RequestExceptionHandler
render_not_found_error('Resource could not be found')
rescue Pundit::NotAuthorizedError
render_unauthorized('You are not authorized to do this action')
+ rescue ActionController::ParameterMissing => e
+ render_could_not_create_error(e.message)
ensure
# to address the thread variable leak issues in Puma/Thin webserver
Current.reset
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 49993e6ee..84677c770 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -45,6 +45,7 @@ class DashboardController < ActionController::Base
@portal = Portal.find_by(custom_domain: domain)
return unless @portal
+ @locale = @portal.default_locale
render 'public/api/v1/portals/show', layout: 'portal', portal: @portal and return
end
diff --git a/app/controllers/devise_overrides/confirmations_controller.rb b/app/controllers/devise_overrides/confirmations_controller.rb
index 1a6dc4209..aaabbcf8c 100644
--- a/app/controllers/devise_overrides/confirmations_controller.rb
+++ b/app/controllers/devise_overrides/confirmations_controller.rb
@@ -14,7 +14,7 @@ class DeviseOverrides::ConfirmationsController < Devise::ConfirmationsController
def render_confirmation_success
send_auth_headers(@confirmable)
- render partial: 'devise/auth.json', locals: { resource: @confirmable }
+ render partial: 'devise/auth', formats: [:json], locals: { resource: @confirmable }
end
def render_confirmation_error
diff --git a/app/controllers/devise_overrides/passwords_controller.rb b/app/controllers/devise_overrides/passwords_controller.rb
index 501b9f90c..06092c5ab 100644
--- a/app/controllers/devise_overrides/passwords_controller.rb
+++ b/app/controllers/devise_overrides/passwords_controller.rb
@@ -11,7 +11,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
@recoverable = User.find_by(reset_password_token: reset_password_token)
if @recoverable && reset_password_and_confirmation(@recoverable)
send_auth_headers(@recoverable)
- render partial: 'devise/auth.json', locals: { resource: @recoverable }
+ render partial: 'devise/auth', formats: [:json], locals: { resource: @recoverable }
else
render json: { message: 'Invalid token', redirect_url: '/' }, status: :unprocessable_entity
end
diff --git a/app/controllers/devise_overrides/sessions_controller.rb b/app/controllers/devise_overrides/sessions_controller.rb
index 3bbc6ee3b..831c41ddc 100644
--- a/app/controllers/devise_overrides/sessions_controller.rb
+++ b/app/controllers/devise_overrides/sessions_controller.rb
@@ -16,7 +16,7 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
end
def render_create_success
- render partial: 'devise/auth.json', locals: { resource: @resource }
+ render partial: 'devise/auth', formats: [:json], locals: { resource: @resource }
end
private
diff --git a/app/controllers/devise_overrides/token_validations_controller.rb b/app/controllers/devise_overrides/token_validations_controller.rb
index b9830d79d..64b7949ac 100644
--- a/app/controllers/devise_overrides/token_validations_controller.rb
+++ b/app/controllers/devise_overrides/token_validations_controller.rb
@@ -2,7 +2,7 @@ class DeviseOverrides::TokenValidationsController < ::DeviseTokenAuth::TokenVali
def validate_token
# @resource will have been set by set_user_by_token concern
if @resource
- render 'devise/token.json'
+ render 'devise/token', formats: [:json]
else
render_validate_token_error
end
diff --git a/app/controllers/platform/api/v1/users_controller.rb b/app/controllers/platform/api/v1/users_controller.rb
index 12c87deb5..2c8995f81 100644
--- a/app/controllers/platform/api/v1/users_controller.rb
+++ b/app/controllers/platform/api/v1/users_controller.rb
@@ -51,6 +51,6 @@ class Platform::Api::V1::UsersController < PlatformController
end
def user_params
- params.permit(:name, :email, :password, custom_attributes: {})
+ params.permit(:name, :display_name, :email, :password, custom_attributes: {})
end
end
diff --git a/app/controllers/public/api/v1/inboxes/contacts_controller.rb b/app/controllers/public/api/v1/inboxes/contacts_controller.rb
index eb794f2a0..1fde3051e 100644
--- a/app/controllers/public/api/v1/inboxes/contacts_controller.rb
+++ b/app/controllers/public/api/v1/inboxes/contacts_controller.rb
@@ -4,7 +4,7 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
def create
source_id = params[:source_id] || SecureRandom.uuid
- @contact_inbox = ::ContactBuilder.new(
+ @contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: source_id,
inbox: @inbox_channel.inbox,
contact_attributes: permitted_params.except(:identifier, :identifier_hash)
diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue
index 7243e566e..0ff94efa5 100644
--- a/app/javascript/dashboard/App.vue
+++ b/app/javascript/dashboard/App.vue
@@ -87,6 +87,9 @@ export default {
},
async initializeAccount() {
await this.$store.dispatch('accounts/get');
+ this.$store.dispatch('setActiveAccount', {
+ accountId: this.currentAccountId,
+ });
const {
locale,
latest_chatwoot_version: latestChatwootVersion,
diff --git a/app/javascript/dashboard/api/agentBots.js b/app/javascript/dashboard/api/agentBots.js
new file mode 100644
index 000000000..4de6fcee0
--- /dev/null
+++ b/app/javascript/dashboard/api/agentBots.js
@@ -0,0 +1,9 @@
+import ApiClient from './ApiClient';
+
+class AgentBotsAPI extends ApiClient {
+ constructor() {
+ super('agent_bots', { accountScoped: true });
+ }
+}
+
+export default new AgentBotsAPI();
diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js
index 76042103f..ef1762f46 100644
--- a/app/javascript/dashboard/api/auth.js
+++ b/app/javascript/dashboard/api/auth.js
@@ -147,4 +147,13 @@ export default {
deleteAvatar() {
return axios.delete(endPoints('deleteAvatar').url);
},
+
+ setActiveAccount({ accountId }) {
+ const urlData = endPoints('setActiveAccount');
+ return axios.put(urlData.url, {
+ profile: {
+ account_id: accountId,
+ },
+ });
+ },
};
diff --git a/app/javascript/dashboard/api/endPoints.js b/app/javascript/dashboard/api/endPoints.js
index c9d0955ef..8deb8d56a 100644
--- a/app/javascript/dashboard/api/endPoints.js
+++ b/app/javascript/dashboard/api/endPoints.js
@@ -40,6 +40,10 @@ const endPoints = {
deleteAvatar: {
url: '/api/v1/profile/avatar',
},
+
+ setActiveAccount: {
+ url: '/api/v1/profile/set_active_account',
+ },
};
export default page => {
diff --git a/app/javascript/dashboard/api/macros.js b/app/javascript/dashboard/api/macros.js
new file mode 100644
index 000000000..7b123c9e8
--- /dev/null
+++ b/app/javascript/dashboard/api/macros.js
@@ -0,0 +1,16 @@
+/* global axios */
+import ApiClient from './ApiClient';
+
+class MacrosAPI extends ApiClient {
+ constructor() {
+ super('macros', { accountScoped: true });
+ }
+
+ executeMacro({ macroId, conversationIds }) {
+ return axios.post(`${this.url}/${macroId}/execute`, {
+ conversation_ids: conversationIds,
+ });
+ }
+}
+
+export default new MacrosAPI();
diff --git a/app/javascript/dashboard/api/specs/agentBots.spec.js b/app/javascript/dashboard/api/specs/agentBots.spec.js
new file mode 100644
index 000000000..c89dbfdf5
--- /dev/null
+++ b/app/javascript/dashboard/api/specs/agentBots.spec.js
@@ -0,0 +1,13 @@
+import AgentBotsAPI from '../agentBots';
+import ApiClient from '../ApiClient';
+
+describe('#AgentBotsAPI', () => {
+ it('creates correct instance', () => {
+ expect(AgentBotsAPI).toBeInstanceOf(ApiClient);
+ expect(AgentBotsAPI).toHaveProperty('get');
+ expect(AgentBotsAPI).toHaveProperty('show');
+ expect(AgentBotsAPI).toHaveProperty('create');
+ expect(AgentBotsAPI).toHaveProperty('update');
+ expect(AgentBotsAPI).toHaveProperty('delete');
+ });
+});
diff --git a/app/javascript/dashboard/api/specs/macros.spec.js b/app/javascript/dashboard/api/specs/macros.spec.js
new file mode 100644
index 000000000..94e936521
--- /dev/null
+++ b/app/javascript/dashboard/api/specs/macros.spec.js
@@ -0,0 +1,14 @@
+import macros from '../macros';
+import ApiClient from '../ApiClient';
+
+describe('#macrosAPI', () => {
+ it('creates correct instance', () => {
+ expect(macros).toBeInstanceOf(ApiClient);
+ expect(macros).toHaveProperty('get');
+ expect(macros).toHaveProperty('create');
+ expect(macros).toHaveProperty('update');
+ expect(macros).toHaveProperty('delete');
+ expect(macros).toHaveProperty('show');
+ expect(macros.url).toBe('/api/v1/macros');
+ });
+});
diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss
index 9d3f84b25..478045000 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss
@@ -113,9 +113,22 @@ $default-button-height: 4.0rem;
}
&.clear {
+ color: var(--w-700);
+
+ &.secondary {
+ color: var(--s-700)
+ }
+
+ &.success {
+ color: var(--g-700)
+ }
+
+ &.alert {
+ color: var(--r-700)
+ }
&.warning {
- color: var(--y-600);
+ color: var(--y-700)
}
&:hover {
@@ -146,6 +159,8 @@ $default-button-height: 4.0rem;
&.small {
height: var(--space-large);
+ padding-bottom: var(--space-smaller);
+ padding-top: var(--space-smaller);
}
&.large {
diff --git a/app/javascript/dashboard/assets/scss/widgets/_modal.scss b/app/javascript/dashboard/assets/scss/widgets/_modal.scss
index fc497f069..543a60797 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_modal.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_modal.scss
@@ -14,15 +14,9 @@
}
.modal--close {
- border-radius: 50%;
- color: $color-heading;
- cursor: pointer;
- font-size: $font-size-big;
- line-height: $space-normal;
- padding: $space-normal;
position: absolute;
- right: $space-micro;
- top: $space-micro;
+ right: $space-small;
+ top: $space-small;
&:hover {
background: $color-background;
diff --git a/app/javascript/dashboard/components/Modal.vue b/app/javascript/dashboard/components/Modal.vue
index f4cad844a..d5dd55ffa 100644
--- a/app/javascript/dashboard/components/Modal.vue
+++ b/app/javascript/dashboard/components/Modal.vue
@@ -7,9 +7,13 @@
@click="onBackDropClick"
>
-
+
diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue
index 314b594d2..b919be2cd 100644
--- a/app/javascript/dashboard/components/layout/Sidebar.vue
+++ b/app/javascript/dashboard/components/layout/Sidebar.vue
@@ -73,14 +73,14 @@ export default {
computed: {
...mapGetters({
- currentUser: 'getCurrentUser',
- globalConfig: 'globalConfig/get',
- isACustomBrandedInstance: 'globalConfig/isACustomBrandedInstance',
- isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
- inboxes: 'inboxes/getInboxes',
accountId: 'getCurrentAccountId',
currentRole: 'getCurrentRole',
+ currentUser: 'getCurrentUser',
+ globalConfig: 'globalConfig/get',
+ inboxes: 'inboxes/getInboxes',
+ isACustomBrandedInstance: 'globalConfig/isACustomBrandedInstance',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
+ isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
labels: 'labels/getLabelsOnSidebar',
teams: 'teams/getMyTeams',
}),
diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js b/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js
index cc4287503..fa4633ec1 100644
--- a/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js
+++ b/app/javascript/dashboard/components/layout/config/sidebarItems/primaryMenu.js
@@ -39,7 +39,7 @@ const primaryMenuItems = accountId => [
label: 'HELP_CENTER.TITLE',
featureFlag: 'help_center',
toState: frontendURL(`accounts/${accountId}/portals`),
- toStateName: 'list_all_portals',
+ toStateName: 'default_portal_articles',
roles: ['administrator'],
},
{
diff --git a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js
index 990b35d4a..768e42ea5 100644
--- a/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js
+++ b/app/javascript/dashboard/components/layout/config/sidebarItems/settings.js
@@ -1,45 +1,58 @@
+import { FEATURE_FLAGS } from '../../../../featureFlags';
import { frontendURL } from '../../../../helper/URLHelper';
const settings = accountId => ({
parentNav: 'settings',
routes: [
+ 'agent_bots',
'agent_list',
- 'canned_list',
- 'labels_list',
- 'settings_inbox',
'attributes_list',
- 'settings_inbox_new',
- 'settings_inbox_list',
- 'settings_inbox_show',
- 'settings_inboxes_page_channel',
- 'settings_inboxes_add_agents',
- 'settings_inbox_finish',
- 'settings_integrations',
- 'settings_integrations_webhook',
- 'settings_integrations_integration',
- 'settings_applications',
- 'settings_integrations_dashboard_apps',
- 'settings_applications_webhook',
- 'settings_applications_integration',
- 'general_settings',
+ 'automation_list',
+ 'billing_settings_index',
+ 'canned_list',
'general_settings_index',
+ 'general_settings',
+ 'labels_list',
+ 'macros_edit',
+ 'macros_new',
+ 'macros_wrapper',
+ 'settings_applications_integration',
+ 'settings_applications_webhook',
+ 'settings_applications',
+ 'settings_inbox_finish',
+ 'settings_inbox_list',
+ 'settings_inbox_new',
+ 'settings_inbox_show',
+ 'settings_inbox',
+ 'settings_inboxes_add_agents',
+ 'settings_inboxes_page_channel',
+ 'settings_integrations_dashboard_apps',
+ 'settings_integrations_integration',
+ 'settings_integrations_webhook',
+ 'settings_integrations',
+ 'settings_teams_add_agents',
+ 'settings_teams_edit_finish',
+ 'settings_teams_edit_members',
+ 'settings_teams_edit',
+ 'settings_teams_finish',
'settings_teams_list',
'settings_teams_new',
- 'settings_teams_add_agents',
- 'settings_teams_finish',
- 'settings_teams_edit',
- 'settings_teams_edit_members',
- 'settings_teams_edit_finish',
- 'billing_settings_index',
- 'automation_list',
],
menuItems: [
+ {
+ icon: 'briefcase',
+ label: 'ACCOUNT_SETTINGS',
+ hasSubMenu: false,
+ toState: frontendURL(`accounts/${accountId}/settings/general`),
+ toStateName: 'general_settings_index',
+ },
{
icon: 'people',
label: 'AGENTS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
toStateName: 'agent_list',
+ featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT,
},
{
icon: 'people-team',
@@ -47,6 +60,7 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
toStateName: 'settings_teams_list',
+ featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT,
},
{
icon: 'mail-inbox-all',
@@ -54,6 +68,7 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
toStateName: 'settings_inbox_list',
+ featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT,
},
{
icon: 'tag',
@@ -61,6 +76,7 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
toStateName: 'labels_list',
+ featureFlag: FEATURE_FLAGS.LABELS,
},
{
icon: 'code',
@@ -70,13 +86,34 @@ const settings = accountId => ({
`accounts/${accountId}/settings/custom-attributes/list`
),
toStateName: 'attributes_list',
+ featureFlag: FEATURE_FLAGS.CUSTOM_ATTRIBUTES,
},
{
icon: 'automation',
label: 'AUTOMATION',
+ beta: true,
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
toStateName: 'automation_list',
+ featureFlag: FEATURE_FLAGS.AUTOMATIONS,
+ },
+ {
+ icon: 'bot',
+ label: 'AGENT_BOTS',
+ beta: true,
+ hasSubMenu: false,
+ toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
+ toStateName: 'agent_bots',
+ featureFlag: FEATURE_FLAGS.AGENT_BOTS,
+ },
+ {
+ icon: 'flash-settings',
+ label: 'MACROS',
+ hasSubMenu: false,
+ toState: frontendURL(`accounts/${accountId}/settings/macros`),
+ toStateName: 'macros_wrapper',
+ beta: true,
+ featureFlag: FEATURE_FLAGS.MACROS,
},
{
icon: 'chat-multiple',
@@ -86,6 +123,7 @@ const settings = accountId => ({
`accounts/${accountId}/settings/canned-response/list`
),
toStateName: 'canned_list',
+ featureFlag: FEATURE_FLAGS.CANNED_RESPONSES,
},
{
icon: 'flash-on',
@@ -93,6 +131,7 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
toStateName: 'settings_integrations',
+ featureFlag: FEATURE_FLAGS.INTEGRATIONS,
},
{
icon: 'star-emphasis',
@@ -100,6 +139,7 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/applications`),
toStateName: 'settings_applications',
+ featureFlag: FEATURE_FLAGS.INTEGRATIONS,
},
{
icon: 'credit-card-person',
@@ -109,13 +149,6 @@ const settings = accountId => ({
toStateName: 'billing_settings_index',
showOnlyOnCloud: true,
},
- {
- icon: 'settings',
- label: 'ACCOUNT_SETTINGS',
- hasSubMenu: false,
- toState: frontendURL(`accounts/${accountId}/settings/general`),
- toStateName: 'general_settings_index',
- },
],
});
diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/AccountSelector.vue b/app/javascript/dashboard/components/layout/sidebarComponents/AccountSelector.vue
index e6e7df48e..ad432f42a 100644
--- a/app/javascript/dashboard/components/layout/sidebarComponents/AccountSelector.vue
+++ b/app/javascript/dashboard/components/layout/sidebarComponents/AccountSelector.vue
@@ -8,25 +8,33 @@
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
/>
-