mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
feat: Ability to snooze conversations (#2682)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
||||||
include Events::Types
|
include Events::Types
|
||||||
|
include DateRangeHelper
|
||||||
|
|
||||||
before_action :conversation, except: [:index, :meta, :search, :create]
|
before_action :conversation, except: [:index, :meta, :search, :create]
|
||||||
before_action :contact_inbox, only: [:create]
|
before_action :contact_inbox, only: [:create]
|
||||||
@@ -49,9 +50,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
|
|
||||||
def toggle_status
|
def toggle_status
|
||||||
if params[:status]
|
if params[:status]
|
||||||
status = params[:status] == 'bot' ? 'pending' : params[:status]
|
set_conversation_status
|
||||||
@conversation.status = status
|
@status = @conversation.save!
|
||||||
@status = @conversation.save
|
|
||||||
else
|
else
|
||||||
@status = @conversation.toggle_status
|
@status = @conversation.toggle_status
|
||||||
end
|
end
|
||||||
@@ -74,6 +74,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_conversation_status
|
||||||
|
status = params[:status] == 'bot' ? 'pending' : params[:status]
|
||||||
|
@conversation.status = status
|
||||||
|
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||||
|
end
|
||||||
|
|
||||||
def trigger_typing_event(event)
|
def trigger_typing_event(event)
|
||||||
user = current_user.presence || @resource
|
user = current_user.presence || @resource
|
||||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
|
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
|
||||||
@@ -115,7 +121,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
inbox_id: @contact_inbox.inbox_id,
|
inbox_id: @contact_inbox.inbox_id,
|
||||||
contact_id: @contact_inbox.contact_id,
|
contact_id: @contact_inbox.contact_id,
|
||||||
contact_inbox_id: @contact_inbox.id,
|
contact_inbox_id: @contact_inbox.id,
|
||||||
additional_attributes: additional_attributes
|
additional_attributes: additional_attributes,
|
||||||
|
snoozed_until: params[:snoozed_until]
|
||||||
}.merge(status)
|
}.merge(status)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,10 @@ class ConversationApi extends ApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleStatus({ conversationId, status }) {
|
toggleStatus({ conversationId, status, snoozedUntil = null }) {
|
||||||
return axios.post(`${this.url}/${conversationId}/toggle_status`, {
|
return axios.post(`${this.url}/${conversationId}/toggle_status`, {
|
||||||
status,
|
status,
|
||||||
|
snoozed_until: snoozedUntil,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ describe('#ConversationAPI', () => {
|
|||||||
`/api/v1/conversations/12/toggle_status`,
|
`/api/v1/conversations/12/toggle_status`,
|
||||||
{
|
{
|
||||||
status: 'online',
|
status: 'online',
|
||||||
|
snoozed_until: null,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
|
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button
|
<woot-button
|
||||||
v-else-if="isPending"
|
v-else-if="showOpenButton"
|
||||||
class-names="resolve"
|
class-names="resolve"
|
||||||
color-scheme="primary"
|
color-scheme="primary"
|
||||||
icon="ion-person"
|
icon="ion-person"
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
|
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="showDropDown"
|
v-if="showAdditionalActions"
|
||||||
:color-scheme="buttonClass"
|
:color-scheme="buttonClass"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
icon="ion-arrow-down-b"
|
icon="ion-arrow-down-b"
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showDropdown"
|
v-if="showActionsDropdown"
|
||||||
v-on-clickaway="closeDropdown"
|
v-on-clickaway="closeDropdown"
|
||||||
class="dropdown-pane dropdown-pane--open"
|
class="dropdown-pane dropdown-pane--open"
|
||||||
>
|
>
|
||||||
@@ -56,6 +56,40 @@
|
|||||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
|
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
|
|
||||||
|
<woot-dropdown-sub-menu
|
||||||
|
v-if="isOpen"
|
||||||
|
:title="this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.TITLE')"
|
||||||
|
>
|
||||||
|
<woot-dropdown-item>
|
||||||
|
<woot-button
|
||||||
|
variant="clear"
|
||||||
|
@click="() => toggleStatus(STATUS_TYPE.SNOOZED, null)"
|
||||||
|
>
|
||||||
|
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.NEXT_REPLY') }}
|
||||||
|
</woot-button>
|
||||||
|
</woot-dropdown-item>
|
||||||
|
<woot-dropdown-item>
|
||||||
|
<woot-button
|
||||||
|
variant="clear"
|
||||||
|
@click="
|
||||||
|
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.tomorrow)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.TOMORROW') }}
|
||||||
|
</woot-button>
|
||||||
|
</woot-dropdown-item>
|
||||||
|
<woot-dropdown-item>
|
||||||
|
<woot-button
|
||||||
|
variant="clear"
|
||||||
|
@click="
|
||||||
|
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.nextWeek)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.NEXT_WEEK') }}
|
||||||
|
</woot-button>
|
||||||
|
</woot-dropdown-item>
|
||||||
|
</woot-dropdown-sub-menu>
|
||||||
</woot-dropdown-menu>
|
</woot-dropdown-menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,20 +101,29 @@ import { mixin as clickaway } from 'vue-clickaway';
|
|||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||||
|
import WootDropdownSubMenu from 'shared/components/ui/dropdown/DropdownSubMenu.vue';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||||
import wootConstants from '../../constants';
|
import wootConstants from '../../constants';
|
||||||
|
import {
|
||||||
|
getUnixTime,
|
||||||
|
addHours,
|
||||||
|
addWeeks,
|
||||||
|
startOfTomorrow,
|
||||||
|
startOfWeek,
|
||||||
|
} from 'date-fns';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootDropdownItem,
|
WootDropdownItem,
|
||||||
WootDropdownMenu,
|
WootDropdownMenu,
|
||||||
|
WootDropdownSubMenu,
|
||||||
},
|
},
|
||||||
mixins: [clickaway, alertMixin],
|
mixins: [clickaway, alertMixin],
|
||||||
props: { conversationId: { type: [String, Number], required: true } },
|
props: { conversationId: { type: [String, Number], required: true } },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
showDropdown: false,
|
showActionsDropdown: false,
|
||||||
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -97,30 +140,47 @@ export default {
|
|||||||
isResolved() {
|
isResolved() {
|
||||||
return this.currentChat.status === wootConstants.STATUS_TYPE.RESOLVED;
|
return this.currentChat.status === wootConstants.STATUS_TYPE.RESOLVED;
|
||||||
},
|
},
|
||||||
|
isSnoozed() {
|
||||||
|
return this.currentChat.status === wootConstants.STATUS_TYPE.SNOOZED;
|
||||||
|
},
|
||||||
buttonClass() {
|
buttonClass() {
|
||||||
if (this.isPending) return 'primary';
|
if (this.isPending) return 'primary';
|
||||||
if (this.isOpen) return 'success';
|
if (this.isOpen) return 'success';
|
||||||
if (this.isResolved) return 'warning';
|
if (this.isResolved) return 'warning';
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
showDropDown() {
|
showAdditionalActions() {
|
||||||
return !this.isPending;
|
return !this.isPending && !this.isSnoozed;
|
||||||
|
},
|
||||||
|
snoozeTimes() {
|
||||||
|
return {
|
||||||
|
// tomorrow = 9AM next day
|
||||||
|
tomorrow: getUnixTime(addHours(startOfTomorrow(), 9)),
|
||||||
|
// next week = 9AM Monday, next week
|
||||||
|
nextWeek: getUnixTime(
|
||||||
|
addHours(startOfWeek(addWeeks(new Date(), 1), { weekStartsOn: 1 }), 9)
|
||||||
|
),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
showOpenButton() {
|
||||||
|
return this.isResolved || this.isSnoozed;
|
||||||
|
},
|
||||||
closeDropdown() {
|
closeDropdown() {
|
||||||
this.showDropdown = false;
|
this.showActionsDropdown = false;
|
||||||
},
|
},
|
||||||
openDropdown() {
|
openDropdown() {
|
||||||
this.showDropdown = true;
|
this.showActionsDropdown = true;
|
||||||
},
|
},
|
||||||
toggleStatus(status) {
|
toggleStatus(status, snoozedUntil) {
|
||||||
this.closeDropdown();
|
this.closeDropdown();
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('toggleStatus', {
|
.dispatch('toggleStatus', {
|
||||||
conversationId: this.currentChat.id,
|
conversationId: this.currentChat.id,
|
||||||
status,
|
status,
|
||||||
|
snoozedUntil,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export default {
|
|||||||
OPEN: 'open',
|
OPEN: 'open',
|
||||||
RESOLVED: 'resolved',
|
RESOLVED: 'resolved',
|
||||||
PENDING: 'pending',
|
PENDING: 'pending',
|
||||||
|
SNOOZED: 'snoozed',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export const DEFAULT_REDIRECT_URL = '/app/';
|
export const DEFAULT_REDIRECT_URL = '/app/';
|
||||||
|
|||||||
@@ -49,6 +49,10 @@
|
|||||||
{
|
{
|
||||||
"TEXT": "Pending",
|
"TEXT": "Pending",
|
||||||
"VALUE": "pending"
|
"VALUE": "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TEXT": "Snoozed",
|
||||||
|
"VALUE": "snoozed"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ATTACHMENTS": {
|
"ATTACHMENTS": {
|
||||||
|
|||||||
@@ -41,7 +41,13 @@
|
|||||||
"DETAILS": "details"
|
"DETAILS": "details"
|
||||||
},
|
},
|
||||||
"RESOLVE_DROPDOWN": {
|
"RESOLVE_DROPDOWN": {
|
||||||
"MARK_PENDING": "Mark as pending"
|
"MARK_PENDING": "Mark as pending",
|
||||||
|
"SNOOZE": {
|
||||||
|
"TITLE": "Snooze until",
|
||||||
|
"NEXT_REPLY": "Next reply",
|
||||||
|
"TOMORROW": "Tomorrow",
|
||||||
|
"NEXT_WEEK": "Next week"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"FOOTER": {
|
"FOOTER": {
|
||||||
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
|
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
|
||||||
|
|||||||
@@ -135,11 +135,15 @@ const actions = {
|
|||||||
commit(types.default.ASSIGN_TEAM, team);
|
commit(types.default.ASSIGN_TEAM, team);
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleStatus: async ({ commit }, { conversationId, status }) => {
|
toggleStatus: async (
|
||||||
|
{ commit },
|
||||||
|
{ conversationId, status, snoozedUntil = null }
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const response = await ConversationApi.toggleStatus({
|
const response = await ConversationApi.toggleStatus({
|
||||||
conversationId,
|
conversationId,
|
||||||
status,
|
status,
|
||||||
|
snoozedUntil,
|
||||||
});
|
});
|
||||||
commit(
|
commit(
|
||||||
types.default.RESOLVE_CONVERSATION,
|
types.default.RESOLVE_CONVERSATION,
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<li class="sub-menu-container">
|
||||||
|
<div class="sub-menu-title">
|
||||||
|
<span class="small">{{ title }}</span>
|
||||||
|
</div>
|
||||||
|
<ul class="sub-menu-li-container">
|
||||||
|
<slot></slot>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'WootDropdownMenu',
|
||||||
|
componentName: 'WootDropdownMenu',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sub-menu-container {
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
margin-top: var(--space-micro);
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sub-menu-title {
|
||||||
|
padding: var(--space-one) var(--space-one) var(--space-smaller);
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
.small {
|
||||||
|
color: var(--b-600);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-menu-li-container {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
class Conversations::ReopenSnoozedConversationsJob < ApplicationJob
|
||||||
|
queue_as :low
|
||||||
|
|
||||||
|
def perform
|
||||||
|
Conversation.where(status: :snoozed).where(snoozed_until: 3.days.ago..Time.current).all.each(&:open!)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,5 +6,8 @@ class TriggerScheduledItemsJob < ApplicationJob
|
|||||||
Campaign.where(campaign_type: :one_off, campaign_status: :active).where(scheduled_at: 3.days.ago..Time.current).all.each do |campaign|
|
Campaign.where(campaign_type: :one_off, campaign_status: :active).where(scheduled_at: 3.days.ago..Time.current).all.each do |campaign|
|
||||||
Campaigns::TriggerOneoffCampaignJob.perform_later(campaign)
|
Campaigns::TriggerOneoffCampaignJob.perform_later(campaign)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Job to reopen snoozed conversations
|
||||||
|
Conversations::ReopenSnoozedConversationsJob.perform_later
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
# contact_last_seen_at :datetime
|
# contact_last_seen_at :datetime
|
||||||
# identifier :string
|
# identifier :string
|
||||||
# last_activity_at :datetime not null
|
# last_activity_at :datetime not null
|
||||||
|
# snoozed_until :datetime
|
||||||
# status :integer default("open"), not null
|
# status :integer default("open"), not null
|
||||||
# uuid :uuid not null
|
# uuid :uuid not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
@@ -45,7 +46,7 @@ class Conversation < ApplicationRecord
|
|||||||
validates :inbox_id, presence: true
|
validates :inbox_id, presence: true
|
||||||
before_validation :validate_additional_attributes
|
before_validation :validate_additional_attributes
|
||||||
|
|
||||||
enum status: { open: 0, resolved: 1, pending: 2 }
|
enum status: { open: 0, resolved: 1, pending: 2, snoozed: 3 }
|
||||||
|
|
||||||
scope :latest, -> { order(last_activity_at: :desc) }
|
scope :latest, -> { order(last_activity_at: :desc) }
|
||||||
scope :unassigned, -> { where(assignee_id: nil) }
|
scope :unassigned, -> { where(assignee_id: nil) }
|
||||||
@@ -64,6 +65,7 @@ class Conversation < ApplicationRecord
|
|||||||
has_one :csat_survey_response, dependent: :destroy
|
has_one :csat_survey_response, dependent: :destroy
|
||||||
has_many :notifications, as: :primary_actor, dependent: :destroy
|
has_many :notifications, as: :primary_actor, dependent: :destroy
|
||||||
|
|
||||||
|
before_save :ensure_snooze_until_reset
|
||||||
before_create :mark_conversation_pending_if_bot
|
before_create :mark_conversation_pending_if_bot
|
||||||
|
|
||||||
# wanted to change this to after_update commit. But it ended up creating a loop
|
# wanted to change this to after_update commit. But it ended up creating a loop
|
||||||
@@ -91,7 +93,7 @@ class Conversation < ApplicationRecord
|
|||||||
def toggle_status
|
def toggle_status
|
||||||
# FIXME: implement state machine with aasm
|
# FIXME: implement state machine with aasm
|
||||||
self.status = open? ? :resolved : :open
|
self.status = open? ? :resolved : :open
|
||||||
self.status = :open if pending?
|
self.status = :open if pending? || snoozed?
|
||||||
save
|
save
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -140,6 +142,10 @@ class Conversation < ApplicationRecord
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def ensure_snooze_until_reset
|
||||||
|
self.snoozed_until = nil unless snoozed?
|
||||||
|
end
|
||||||
|
|
||||||
def validate_additional_attributes
|
def validate_additional_attributes
|
||||||
self.additional_attributes = {} unless additional_attributes.is_a?(Hash)
|
self.additional_attributes = {} unless additional_attributes.is_a?(Hash)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -164,7 +164,10 @@ class Message < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reopen_conversation
|
def reopen_conversation
|
||||||
conversation.open! if incoming? && conversation.resolved? && !conversation.muted?
|
return if conversation.muted?
|
||||||
|
return unless incoming?
|
||||||
|
|
||||||
|
conversation.open! if conversation.resolved? || conversation.snoozed?
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute_message_template_hooks
|
def execute_message_template_hooks
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ en:
|
|||||||
resolved: "Conversation was marked resolved by %{user_name}"
|
resolved: "Conversation was marked resolved by %{user_name}"
|
||||||
open: "Conversation was reopened by %{user_name}"
|
open: "Conversation was reopened by %{user_name}"
|
||||||
pending: "Conversation was marked as pending by %{user_name}"
|
pending: "Conversation was marked as pending by %{user_name}"
|
||||||
|
snoozed: "Conversation was snoozed by %{user_name}"
|
||||||
auto_resolved: "Conversation was marked resolved by system due to %{duration} days of inactivity"
|
auto_resolved: "Conversation was marked resolved by system due to %{duration} days of inactivity"
|
||||||
assignee:
|
assignee:
|
||||||
self_assigned: "%{user_name} self-assigned this conversation"
|
self_assigned: "%{user_name} self-assigned this conversation"
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class AddSnoozedUntilToConversations < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
add_column :conversations, :snoozed_until, :datetime
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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.define(version: 2021_07_14_110714) do
|
ActiveRecord::Schema.define(version: 2021_07_21_182458) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
@@ -267,6 +267,7 @@ ActiveRecord::Schema.define(version: 2021_07_14_110714) do
|
|||||||
t.datetime "last_activity_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
|
t.datetime "last_activity_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
|
||||||
t.bigint "team_id"
|
t.bigint "team_id"
|
||||||
t.bigint "campaign_id"
|
t.bigint "campaign_id"
|
||||||
|
t.datetime "snoozed_until"
|
||||||
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
|
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
|
||||||
t.index ["account_id"], name: "index_conversations_on_account_id"
|
t.index ["account_id"], name: "index_conversations_on_account_id"
|
||||||
t.index ["campaign_id"], name: "index_conversations_on_campaign_id"
|
t.index ["campaign_id"], name: "index_conversations_on_campaign_id"
|
||||||
|
|||||||
@@ -306,6 +306,19 @@ RSpec.describe 'Conversations API', type: :request do
|
|||||||
expect(conversation.reload.status).to eq('pending')
|
expect(conversation.reload.status).to eq('pending')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'toggles the conversation status to snoozed when parameter is passed' do
|
||||||
|
expect(conversation.status).to eq('open')
|
||||||
|
snoozed_until = (DateTime.now.utc + 2.days).to_i
|
||||||
|
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
params: { status: 'snoozed', snoozed_until: snoozed_until },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(conversation.reload.status).to eq('snoozed')
|
||||||
|
expect(conversation.reload.snoozed_until.to_i).to eq(snoozed_until)
|
||||||
|
end
|
||||||
|
|
||||||
# TODO: remove this spec when we remove the condition check in controller
|
# TODO: remove this spec when we remove the condition check in controller
|
||||||
# Added for backwards compatibility for bot status
|
# Added for backwards compatibility for bot status
|
||||||
it 'toggles the conversation status to pending status when parameter bot is passed' do
|
it 'toggles the conversation status to pending status when parameter bot is passed' do
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Conversations::ReopenSnoozedConversationsJob, type: :job do
|
||||||
|
let!(:snoozed_till_5_minutes_ago) { create(:conversation, status: :snoozed, snoozed_until: 5.minutes.ago) }
|
||||||
|
let!(:snoozed_till_tomorrow) { create(:conversation, status: :snoozed, snoozed_until: 1.day.from_now) }
|
||||||
|
let!(:snoozed_indefinitely) { create(:conversation, status: :snoozed) }
|
||||||
|
|
||||||
|
it 'enqueues the job' do
|
||||||
|
expect { described_class.perform_later }.to have_enqueued_job(described_class)
|
||||||
|
.on_queue('low')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when called' do
|
||||||
|
it 'reopens snoozed conversations whose snooze until has passed' do
|
||||||
|
described_class.perform_now
|
||||||
|
|
||||||
|
expect(snoozed_till_5_minutes_ago.reload.status).to eq 'open'
|
||||||
|
expect(snoozed_till_tomorrow.reload.status).to eq 'snoozed'
|
||||||
|
expect(snoozed_indefinitely.reload.status).to eq 'snoozed'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -20,5 +20,10 @@ RSpec.describe TriggerScheduledItemsJob, type: :job do
|
|||||||
expect(Campaigns::TriggerOneoffCampaignJob).to receive(:perform_later).with(campaign).once
|
expect(Campaigns::TriggerOneoffCampaignJob).to receive(:perform_later).with(campaign).once
|
||||||
described_class.perform_now
|
described_class.perform_now
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'triggers Conversations::ReopenSnoozedConversationsJob' do
|
||||||
|
expect(Conversations::ReopenSnoozedConversationsJob).to receive(:perform_later).once
|
||||||
|
described_class.perform_now
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -181,14 +181,38 @@ RSpec.describe Conversation, type: :model do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe '#toggle_status' do
|
describe '#toggle_status' do
|
||||||
subject(:toggle_status) { conversation.toggle_status }
|
it 'toggles conversation status to resolved when open' do
|
||||||
|
conversation = create(:conversation, status: 'open')
|
||||||
let(:conversation) { create(:conversation, status: :open) }
|
expect(conversation.toggle_status).to eq(true)
|
||||||
|
|
||||||
it 'toggles conversation status' do
|
|
||||||
expect(toggle_status).to eq(true)
|
|
||||||
expect(conversation.reload.status).to eq('resolved')
|
expect(conversation.reload.status).to eq('resolved')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'toggles conversation status to open when resolved' do
|
||||||
|
conversation = create(:conversation, status: 'resolved')
|
||||||
|
expect(conversation.toggle_status).to eq(true)
|
||||||
|
expect(conversation.reload.status).to eq('open')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'toggles conversation status to open when pending' do
|
||||||
|
conversation = create(:conversation, status: 'pending')
|
||||||
|
expect(conversation.toggle_status).to eq(true)
|
||||||
|
expect(conversation.reload.status).to eq('open')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'toggles conversation status to open when snoozed' do
|
||||||
|
conversation = create(:conversation, status: 'snoozed')
|
||||||
|
expect(conversation.toggle_status).to eq(true)
|
||||||
|
expect(conversation.reload.status).to eq('open')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#ensure_snooze_until_reset' do
|
||||||
|
it 'resets the snoozed_until when status is toggled' do
|
||||||
|
conversation = create(:conversation, status: 'snoozed', snoozed_until: 2.days.from_now)
|
||||||
|
expect(conversation.snoozed_until).not_to eq nil
|
||||||
|
expect(conversation.toggle_status).to eq(true)
|
||||||
|
expect(conversation.reload.snoozed_until).to eq(nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#mute!' do
|
describe '#mute!' do
|
||||||
|
|||||||
@@ -9,6 +9,30 @@ RSpec.describe Message, type: :model do
|
|||||||
it { is_expected.to validate_presence_of(:account_id) }
|
it { is_expected.to validate_presence_of(:account_id) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#reopen_conversation' do
|
||||||
|
let(:conversation) { create(:conversation) }
|
||||||
|
let(:message) { build(:message, message_type: :incoming, conversation: conversation) }
|
||||||
|
|
||||||
|
it 'reopens resolved conversation when the message is from a contact' do
|
||||||
|
conversation.resolved!
|
||||||
|
message.save!
|
||||||
|
expect(message.conversation.open?).to eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'reopens snoozed conversation when the message is from a contact' do
|
||||||
|
conversation.snoozed!
|
||||||
|
message.save!
|
||||||
|
expect(message.conversation.open?).to eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'will not reopen if the conversation is muted' do
|
||||||
|
conversation.resolved!
|
||||||
|
conversation.mute!
|
||||||
|
message.save!
|
||||||
|
expect(message.conversation.open?).to eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when message is created' do
|
context 'when message is created' do
|
||||||
let(:message) { build(:message, account: create(:account)) }
|
let(:message) { build(:message, account: create(:account)) }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user