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:
@@ -28,9 +28,10 @@ class ConversationApi extends ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
toggleStatus({ conversationId, status }) {
|
||||
toggleStatus({ conversationId, status, snoozedUntil = null }) {
|
||||
return axios.post(`${this.url}/${conversationId}/toggle_status`, {
|
||||
status,
|
||||
snoozed_until: snoozedUntil,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ describe('#ConversationAPI', () => {
|
||||
`/api/v1/conversations/12/toggle_status`,
|
||||
{
|
||||
status: 'online',
|
||||
snoozed_until: null,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{{ this.$t('CONVERSATION.HEADER.REOPEN_ACTION') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-else-if="isPending"
|
||||
v-else-if="showOpenButton"
|
||||
class-names="resolve"
|
||||
color-scheme="primary"
|
||||
icon="ion-person"
|
||||
@@ -34,7 +34,7 @@
|
||||
{{ this.$t('CONVERSATION.HEADER.OPEN_ACTION') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="showDropDown"
|
||||
v-if="showAdditionalActions"
|
||||
:color-scheme="buttonClass"
|
||||
:disabled="isLoading"
|
||||
icon="ion-arrow-down-b"
|
||||
@@ -43,7 +43,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showDropdown"
|
||||
v-if="showActionsDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="dropdown-pane dropdown-pane--open"
|
||||
>
|
||||
@@ -56,6 +56,40 @@
|
||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
|
||||
</woot-button>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,20 +101,29 @@ import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
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 wootConstants from '../../constants';
|
||||
import {
|
||||
getUnixTime,
|
||||
addHours,
|
||||
addWeeks,
|
||||
startOfTomorrow,
|
||||
startOfWeek,
|
||||
} from 'date-fns';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
WootDropdownSubMenu,
|
||||
},
|
||||
mixins: [clickaway, alertMixin],
|
||||
props: { conversationId: { type: [String, Number], required: true } },
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
showDropdown: false,
|
||||
showActionsDropdown: false,
|
||||
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
||||
};
|
||||
},
|
||||
@@ -97,30 +140,47 @@ export default {
|
||||
isResolved() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.RESOLVED;
|
||||
},
|
||||
isSnoozed() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.SNOOZED;
|
||||
},
|
||||
buttonClass() {
|
||||
if (this.isPending) return 'primary';
|
||||
if (this.isOpen) return 'success';
|
||||
if (this.isResolved) return 'warning';
|
||||
return '';
|
||||
},
|
||||
showDropDown() {
|
||||
return !this.isPending;
|
||||
showAdditionalActions() {
|
||||
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: {
|
||||
showOpenButton() {
|
||||
return this.isResolved || this.isSnoozed;
|
||||
},
|
||||
closeDropdown() {
|
||||
this.showDropdown = false;
|
||||
this.showActionsDropdown = false;
|
||||
},
|
||||
openDropdown() {
|
||||
this.showDropdown = true;
|
||||
this.showActionsDropdown = true;
|
||||
},
|
||||
toggleStatus(status) {
|
||||
toggleStatus(status, snoozedUntil) {
|
||||
this.closeDropdown();
|
||||
this.isLoading = true;
|
||||
this.$store
|
||||
.dispatch('toggleStatus', {
|
||||
conversationId: this.currentChat.id,
|
||||
status,
|
||||
snoozedUntil,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||
|
||||
@@ -9,6 +9,7 @@ export default {
|
||||
OPEN: 'open',
|
||||
RESOLVED: 'resolved',
|
||||
PENDING: 'pending',
|
||||
SNOOZED: 'snoozed',
|
||||
},
|
||||
};
|
||||
export const DEFAULT_REDIRECT_URL = '/app/';
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
{
|
||||
"TEXT": "Pending",
|
||||
"VALUE": "pending"
|
||||
},
|
||||
{
|
||||
"TEXT": "Snoozed",
|
||||
"VALUE": "snoozed"
|
||||
}
|
||||
],
|
||||
"ATTACHMENTS": {
|
||||
|
||||
@@ -41,7 +41,13 @@
|
||||
"DETAILS": "details"
|
||||
},
|
||||
"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": {
|
||||
"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);
|
||||
},
|
||||
|
||||
toggleStatus: async ({ commit }, { conversationId, status }) => {
|
||||
toggleStatus: async (
|
||||
{ commit },
|
||||
{ conversationId, status, snoozedUntil = null }
|
||||
) => {
|
||||
try {
|
||||
const response = await ConversationApi.toggleStatus({
|
||||
conversationId,
|
||||
status,
|
||||
snoozedUntil,
|
||||
});
|
||||
commit(
|
||||
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>
|
||||
Reference in New Issue
Block a user