feat: Ability to lock to single conversation (#5881)
Adds the ability to lock conversation to a single thread for Whatsapp and Sms Inboxes when using outbound messages. demo: https://www.loom.com/share/c9e1e563c8914837a4139dfdd2503fef fixes: #4975 Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com>
This commit is contained in:
parent
8813c77907
commit
b05d06a28a
|
@ -16,7 +16,6 @@ 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:
|
||||
|
|
40
app/builders/conversation_builder.rb
Normal file
40
app/builders/conversation_builder.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
class ConversationBuilder
|
||||
pattr_initialize [:params!, :contact_inbox!]
|
||||
|
||||
def perform
|
||||
look_up_exising_conversation || create_new_conversation
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def look_up_exising_conversation
|
||||
return unless @contact_inbox.inbox.lock_to_single_conversation?
|
||||
|
||||
@contact_inbox.conversations.last
|
||||
end
|
||||
|
||||
def create_new_conversation
|
||||
::Conversation.create!(conversation_params)
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||
custom_attributes = params[:custom_attributes]&.permit! || {}
|
||||
status = params[:status].present? ? { status: params[:status] } : {}
|
||||
|
||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
|
||||
# status = { status: 'pending' } if status[:status] == 'bot'
|
||||
{
|
||||
account_id: @contact_inbox.inbox.account_id,
|
||||
inbox_id: @contact_inbox.inbox_id,
|
||||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: additional_attributes,
|
||||
custom_attributes: custom_attributes,
|
||||
snoozed_until: params[:snoozed_until],
|
||||
assignee_id: params[:assignee_id],
|
||||
team_id: params[:team_id]
|
||||
}.merge(status)
|
||||
end
|
||||
end
|
|
@ -24,7 +24,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
@conversation = ::Conversation.create!(conversation_params)
|
||||
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform
|
||||
Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present?
|
||||
end
|
||||
end
|
||||
|
@ -99,8 +99,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
end
|
||||
|
||||
def set_conversation_status
|
||||
status = params[:status] == 'bot' ? 'pending' : params[:status]
|
||||
@conversation.status = status
|
||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
|
||||
# status = params[:status] == 'bot' ? 'pending' : params[:status]
|
||||
@conversation.status = params[:status]
|
||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||
end
|
||||
|
||||
|
@ -152,26 +154,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
).perform
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||
custom_attributes = params[:custom_attributes]&.permit! || {}
|
||||
status = params[:status].present? ? { status: params[:status] } : {}
|
||||
|
||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||
status = { status: 'pending' } if status[:status] == 'bot'
|
||||
{
|
||||
account_id: Current.account.id,
|
||||
inbox_id: @contact_inbox.inbox_id,
|
||||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: additional_attributes,
|
||||
custom_attributes: custom_attributes,
|
||||
snoozed_until: params[:snoozed_until],
|
||||
assignee_id: params[:assignee_id],
|
||||
team_id: params[:team_id]
|
||||
}.merge(status)
|
||||
end
|
||||
|
||||
def conversation_finder
|
||||
@conversation_finder ||= ConversationFinder.new(Current.user, params)
|
||||
end
|
||||
|
|
|
@ -113,7 +113,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
|
||||
def inbox_attributes
|
||||
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved]
|
||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
||||
:lock_to_single_conversation]
|
||||
end
|
||||
|
||||
def permitted_params(channel_attributes = [])
|
||||
|
|
|
@ -388,6 +388,10 @@
|
|||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled"
|
||||
},
|
||||
"LOCK_TO_SINGLE_CONVERSATION": {
|
||||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled"
|
||||
},
|
||||
"ENABLE_HMAC": {
|
||||
"LABEL": "Enable"
|
||||
}
|
||||
|
@ -441,6 +445,8 @@
|
|||
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
|
||||
"ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email",
|
||||
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.",
|
||||
"LOCK_TO_SINGLE_CONVERSATION": "Lock to single conversation",
|
||||
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "Enable or disable multiple conversations for the same contact in this inbox",
|
||||
"INBOX_UPDATE_TITLE": "Inbox Settings",
|
||||
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings",
|
||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of new conversations to the agents added to this inbox.",
|
||||
|
|
|
@ -258,6 +258,28 @@
|
|||
</p>
|
||||
</label>
|
||||
|
||||
<label
|
||||
v-if="canLocktoSingleConversation"
|
||||
class="medium-9 columns settings-item"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.LOCK_TO_SINGLE_CONVERSATION') }}
|
||||
<select v-model="locktoSingleConversation">
|
||||
<option :value="true">
|
||||
{{ $t('INBOX_MGMT.EDIT.LOCK_TO_SINGLE_CONVERSATION.ENABLED') }}
|
||||
</option>
|
||||
<option :value="false">
|
||||
{{ $t('INBOX_MGMT.EDIT.LOCK_TO_SINGLE_CONVERSATION.DISABLED') }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="help-text">
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</label>
|
||||
|
||||
<label v-if="isAWebWidgetInbox">
|
||||
{{ $t('INBOX_MGMT.FEATURES.LABEL') }}
|
||||
</label>
|
||||
|
@ -380,6 +402,7 @@ export default {
|
|||
greetingMessage: '',
|
||||
emailCollectEnabled: false,
|
||||
csatSurveyEnabled: false,
|
||||
locktoSingleConversation: false,
|
||||
allowMessagesAfterResolved: true,
|
||||
continuityViaEmail: true,
|
||||
selectedInboxName: '',
|
||||
|
@ -496,6 +519,9 @@ export default {
|
|||
}
|
||||
return this.inbox.name;
|
||||
},
|
||||
canLocktoSingleConversation() {
|
||||
return this.isASmsInbox || this.isAWhatsAppChannel;
|
||||
},
|
||||
inboxNameLabel() {
|
||||
if (this.isAWebWidgetInbox) {
|
||||
return this.$t('INBOX_MGMT.ADD.WEBSITE_NAME.LABEL');
|
||||
|
@ -567,6 +593,7 @@ export default {
|
|||
this.channelWelcomeTagline = this.inbox.welcome_tagline;
|
||||
this.selectedFeatureFlags = this.inbox.selected_feature_flags || [];
|
||||
this.replyTime = this.inbox.reply_time;
|
||||
this.locktoSingleConversation = this.inbox.lock_to_single_conversation;
|
||||
});
|
||||
},
|
||||
async updateInbox() {
|
||||
|
@ -579,6 +606,7 @@ export default {
|
|||
allow_messages_after_resolved: this.allowMessagesAfterResolved,
|
||||
greeting_enabled: this.greetingEnabled,
|
||||
greeting_message: this.greetingMessage || '',
|
||||
lock_to_single_conversation: this.locktoSingleConversation,
|
||||
channel: {
|
||||
widget_color: this.inbox.widget_color,
|
||||
website_url: this.channelWebsiteUrl,
|
||||
|
|
|
@ -89,7 +89,19 @@ export const mutations = {
|
|||
},
|
||||
[types.default.ADD_CONTACT_CONVERSATION]: ($state, { id, data }) => {
|
||||
const conversations = $state.records[id] || [];
|
||||
Vue.set($state.records, id, [...conversations, data]);
|
||||
|
||||
const updatedConversations = [...conversations];
|
||||
const index = conversations.findIndex(
|
||||
conversation => conversation.id === data.id
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
updatedConversations[index] = { ...conversations[index], ...data };
|
||||
} else {
|
||||
updatedConversations.push(data);
|
||||
}
|
||||
|
||||
Vue.set($state.records, id, updatedConversations);
|
||||
},
|
||||
[types.default.DELETE_CONTACT_CONVERSATION]: ($state, id) => {
|
||||
Vue.delete($state.records, id);
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# enable_email_collect :boolean default(TRUE)
|
||||
# greeting_enabled :boolean default(FALSE)
|
||||
# greeting_message :string
|
||||
# lock_to_single_conversation :boolean default(FALSE), not null
|
||||
# name :string not null
|
||||
# out_of_office_message :string
|
||||
# timezone :string default("UTC")
|
||||
|
|
|
@ -15,12 +15,13 @@ json.working_hours resource.weekly_schedule
|
|||
json.timezone resource.timezone
|
||||
json.callback_webhook_url resource.callback_webhook_url
|
||||
json.allow_messages_after_resolved resource.allow_messages_after_resolved
|
||||
|
||||
json.tweets_enabled resource.channel.try(:tweets_enabled) if resource.twitter?
|
||||
json.lock_to_single_conversation resource.lock_to_single_conversation
|
||||
|
||||
## Channel specific settings
|
||||
## TODO : Clean up and move the attributes into channel sub section
|
||||
|
||||
json.tweets_enabled resource.channel.try(:tweets_enabled) if resource.twitter?
|
||||
|
||||
## WebWidget Attributes
|
||||
json.widget_color resource.channel.try(:widget_color)
|
||||
json.website_url resource.channel.try(:website_url)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class AddLockConversationToSingleThread < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :inboxes, :lock_to_single_conversation, :boolean, null: false, default: false
|
||||
end
|
||||
end
|
|
@ -535,6 +535,7 @@ ActiveRecord::Schema.define(version: 2022_11_16_000514) do
|
|||
t.boolean "csat_survey_enabled", default: false
|
||||
t.boolean "allow_messages_after_resolved", default: true
|
||||
t.jsonb "auto_assignment_config", default: {}
|
||||
t.boolean "lock_to_single_conversation", default: false, null: false
|
||||
t.index ["account_id"], name: "index_inboxes_on_account_id"
|
||||
end
|
||||
|
||||
|
|
46
spec/builders/conversation_builder_spec.rb
Normal file
46
spec/builders/conversation_builder_spec.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ::ConversationBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let!(:sms_channel) { create(:channel_sms, account: account) }
|
||||
let!(:sms_inbox) { create(:inbox, channel: sms_channel, account: account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: sms_inbox) }
|
||||
|
||||
describe '#perform' do
|
||||
it 'creates conversation' do
|
||||
conversation = described_class.new(
|
||||
contact_inbox: contact_inbox,
|
||||
params: {}
|
||||
).perform
|
||||
|
||||
expect(conversation.contact_inbox_id).to eq(contact_inbox.id)
|
||||
end
|
||||
|
||||
context 'when lock_to_single_conversation is true for inbox' do
|
||||
before do
|
||||
sms_inbox.update!(lock_to_single_conversation: true)
|
||||
end
|
||||
|
||||
it 'creates conversation when existing conversation is not present' do
|
||||
conversation = described_class.new(
|
||||
contact_inbox: contact_inbox,
|
||||
params: {}
|
||||
).perform
|
||||
|
||||
expect(conversation.contact_inbox_id).to eq(contact_inbox.id)
|
||||
end
|
||||
|
||||
it 'returns last from existing conversations when existing conversation is not present' do
|
||||
create(:conversation, contact_inbox: contact_inbox)
|
||||
existing_conversation = create(:conversation, contact_inbox: contact_inbox)
|
||||
conversation = described_class.new(
|
||||
contact_inbox: contact_inbox,
|
||||
params: {}
|
||||
).perform
|
||||
|
||||
expect(conversation.id).to eq(existing_conversation.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -265,17 +265,18 @@ RSpec.describe 'Conversations API', type: :request do
|
|||
|
||||
# TODO: remove this spec when we remove the condition check in controller
|
||||
# Added for backwards compatibility for bot status
|
||||
it 'creates a conversation as pending if status is specified as bot' do
|
||||
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||
post "/api/v1/accounts/#{account.id}/conversations",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { source_id: contact_inbox.source_id, status: 'bot' },
|
||||
as: :json
|
||||
# remove this in subsequent release
|
||||
# it 'creates a conversation as pending if status is specified as bot' do
|
||||
# allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||
# post "/api/v1/accounts/#{account.id}/conversations",
|
||||
# headers: agent.create_new_auth_token,
|
||||
# params: { source_id: contact_inbox.source_id, status: 'bot' },
|
||||
# as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body, symbolize_names: true)
|
||||
expect(response_data[:status]).to eq('pending')
|
||||
end
|
||||
# expect(response).to have_http_status(:success)
|
||||
# response_data = JSON.parse(response.body, symbolize_names: true)
|
||||
# expect(response_data[:status]).to eq('pending')
|
||||
# end
|
||||
|
||||
it 'creates a new conversation with message when message is passed' do
|
||||
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||
|
@ -408,17 +409,18 @@ RSpec.describe 'Conversations API', type: :request do
|
|||
|
||||
# TODO: remove this spec when we remove the condition check in controller
|
||||
# Added for backwards compatibility for bot status
|
||||
it 'toggles the conversation status to pending status when parameter bot is passed' do
|
||||
expect(conversation.status).to eq('open')
|
||||
# remove in next release
|
||||
# it 'toggles the conversation status to pending status when parameter bot is passed' do
|
||||
# expect(conversation.status).to eq('open')
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { status: 'bot' },
|
||||
as: :json
|
||||
# post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
|
||||
# headers: agent.create_new_auth_token,
|
||||
# params: { status: 'bot' },
|
||||
# as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.status).to eq('pending')
|
||||
end
|
||||
# expect(response).to have_http_status(:success)
|
||||
# expect(conversation.reload.status).to eq('pending')
|
||||
# end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue