feat(conversations): add message creation limit lock
This commit is contained in:
@@ -125,6 +125,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isMessageCreationLocked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'toggleInsertArticle',
|
||||
@@ -250,7 +254,16 @@ export default {
|
||||
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
|
||||
},
|
||||
enableInsertArticleInReply() {
|
||||
return this.portalSlug;
|
||||
return !this.isMessageCreationLocked && this.portalSlug;
|
||||
},
|
||||
showQuotedReplyButton() {
|
||||
return this.showQuotedReplyToggle && !this.isMessageCreationLocked;
|
||||
},
|
||||
showWhatsAppTemplateButton() {
|
||||
return this.enableWhatsAppTemplates && !this.isMessageCreationLocked;
|
||||
},
|
||||
showContentTemplateButton() {
|
||||
return this.enableContentTemplates && !this.isMessageCreationLocked;
|
||||
},
|
||||
isFetchingAppIntegrations() {
|
||||
return this.uiFlags.isFetching;
|
||||
@@ -340,7 +353,7 @@ export default {
|
||||
@click="toggleMessageSignature"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="showQuotedReplyToggle"
|
||||
v-if="showQuotedReplyButton"
|
||||
v-tooltip.top-end="quotedReplyToggleTooltip"
|
||||
icon="i-ph-quotes"
|
||||
:variant="quotedReplyEnabled ? 'solid' : 'faded'"
|
||||
@@ -350,7 +363,7 @@ export default {
|
||||
@click="$emit('toggleQuotedReply')"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="enableWhatsAppTemplates"
|
||||
v-if="showWhatsAppTemplateButton"
|
||||
v-tooltip.top-end="$t('CONVERSATION.FOOTER.WHATSAPP_TEMPLATES')"
|
||||
icon="i-ph-whatsapp-logo"
|
||||
slate
|
||||
@@ -359,7 +372,7 @@ export default {
|
||||
@click="$emit('selectWhatsappTemplate')"
|
||||
/>
|
||||
<NextButton
|
||||
v-if="enableContentTemplates"
|
||||
v-if="showContentTemplateButton"
|
||||
v-tooltip.top-end="'Content Templates'"
|
||||
icon="i-ph-whatsapp-logo"
|
||||
slate
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue';
|
||||
import CopilotEditor from 'dashboard/components/widgets/WootWriter/CopilotEditor.vue';
|
||||
import CaptainLoader from 'dashboard/components/widgets/conversation/copilot/CaptainLoader.vue';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
showCopilotEditor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -16,6 +16,10 @@ defineProps({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isMessageCreationLocked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -41,6 +45,8 @@ const clearEditorSelection = () => {
|
||||
};
|
||||
|
||||
const onSend = () => {
|
||||
if (props.isMessageCreationLocked) return;
|
||||
|
||||
emit('send', copilotEditorContent.value);
|
||||
copilotEditorContent.value = '';
|
||||
};
|
||||
@@ -58,7 +64,9 @@ const onSend = () => {
|
||||
@after-enter="emit('contentReady')"
|
||||
>
|
||||
<CopilotEditor
|
||||
v-if="showCopilotEditor && !isGeneratingContent"
|
||||
v-if="
|
||||
showCopilotEditor && !isGeneratingContent && !isMessageCreationLocked
|
||||
"
|
||||
key="copilot-editor"
|
||||
v-model="copilotEditorContent"
|
||||
class="copilot-editor"
|
||||
|
||||
@@ -193,6 +193,19 @@ export default {
|
||||
}
|
||||
return this.$t('CONVERSATION.CANNOT_REPLY');
|
||||
},
|
||||
messageCreationLockBannerMessage() {
|
||||
if (!this.currentChat?.message_creation_locked) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (this.currentChat.message_creation_lock_reason === 'message_limit') {
|
||||
return this.$t('CONVERSATION.MESSAGE_CREATION_LOCK.MESSAGE_LIMIT', {
|
||||
limit: this.currentChat.message_limit,
|
||||
});
|
||||
}
|
||||
|
||||
return this.$t('CONVERSATION.MESSAGE_CREATION_LOCK.MANUAL');
|
||||
},
|
||||
replyWindowLink() {
|
||||
if (this.isAFacebookInbox || this.isAnInstagramChannel) {
|
||||
return REPLY_POLICY.FACEBOOK;
|
||||
@@ -435,6 +448,8 @@ export default {
|
||||
},
|
||||
async handleMessageRetry(message) {
|
||||
if (!message) return;
|
||||
if (this.currentChat.message_creation_locked) return;
|
||||
|
||||
const payload = useSnakeCase(message);
|
||||
await this.$store.dispatch('sendMessageWithData', payload);
|
||||
},
|
||||
@@ -455,7 +470,13 @@ export default {
|
||||
>
|
||||
<div ref="topBannerRef">
|
||||
<Banner
|
||||
v-if="!currentChat.can_reply"
|
||||
v-if="currentChat.message_creation_locked"
|
||||
color-scheme="alert"
|
||||
class="mx-2 mt-2 overflow-hidden rounded-lg"
|
||||
:banner-message="messageCreationLockBannerMessage"
|
||||
/>
|
||||
<Banner
|
||||
v-else-if="!currentChat.can_reply"
|
||||
color-scheme="alert"
|
||||
class="mx-2 mt-2 overflow-hidden rounded-lg"
|
||||
:banner-message="replyWindowBannerMessage"
|
||||
|
||||
@@ -201,6 +201,9 @@ export default {
|
||||
!(this.isAWhatsAppChannel || this.isAPIInbox)
|
||||
);
|
||||
},
|
||||
isMessageCreationLocked() {
|
||||
return !!this.currentChat?.message_creation_locked;
|
||||
},
|
||||
inboxId() {
|
||||
return this.currentChat.inbox_id;
|
||||
},
|
||||
@@ -209,6 +212,9 @@ export default {
|
||||
},
|
||||
messagePlaceHolder() {
|
||||
if (this.isEditorDisabled) {
|
||||
if (this.isMessageCreationLocked) {
|
||||
return this.$t('CONVERSATION.FOOTER.MESSAGE_CREATION_LOCKED');
|
||||
}
|
||||
if (this.isAWhatsAppChannel) {
|
||||
return this.$t('CONVERSATION.FOOTER.MESSAGING_RESTRICTED_WHATSAPP');
|
||||
}
|
||||
@@ -326,7 +332,11 @@ export default {
|
||||
return !this.isOnPrivateNote && this.showFileUpload;
|
||||
},
|
||||
showAudioRecorderEditor() {
|
||||
return this.showAudioRecorder && this.isRecordingAudio;
|
||||
return (
|
||||
!this.isMessageCreationLocked &&
|
||||
this.showAudioRecorder &&
|
||||
this.isRecordingAudio
|
||||
);
|
||||
},
|
||||
isOnPrivateNote() {
|
||||
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
||||
@@ -439,6 +449,10 @@ export default {
|
||||
return !this.showAudioRecorderEditor && !this.copilot.isActive.value;
|
||||
},
|
||||
isEditorDisabled() {
|
||||
if (this.isMessageCreationLocked) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
(this.isAWhatsAppChannel || this.isAPIInbox) &&
|
||||
!this.isOnPrivateNote &&
|
||||
@@ -804,6 +818,8 @@ export default {
|
||||
}
|
||||
},
|
||||
sendMessageAsMultipleMessages(message, copilotAcceptedMessage = '') {
|
||||
if (this.isMessageCreationLocked) return;
|
||||
|
||||
const messages = this.getMultipleMessagesPayload(message);
|
||||
messages.forEach(messagePayload => {
|
||||
this.sendMessage(
|
||||
@@ -888,6 +904,8 @@ export default {
|
||||
editorMessage = '',
|
||||
copilotAcceptedMessage = ''
|
||||
) {
|
||||
if (this.isMessageCreationLocked) return;
|
||||
|
||||
try {
|
||||
await this.$store.dispatch(
|
||||
'createPendingMessageAndSend',
|
||||
@@ -907,6 +925,8 @@ export default {
|
||||
}
|
||||
},
|
||||
async onSendWhatsAppReply(messagePayload) {
|
||||
if (this.isMessageCreationLocked) return;
|
||||
|
||||
this.sendMessage({
|
||||
conversationId: this.currentChat.id,
|
||||
...messagePayload,
|
||||
@@ -914,6 +934,8 @@ export default {
|
||||
this.hideWhatsappTemplatesModal();
|
||||
},
|
||||
async onSendContentTemplateReply(messagePayload) {
|
||||
if (this.isMessageCreationLocked) return;
|
||||
|
||||
this.sendMessage({
|
||||
conversationId: this.currentChat.id,
|
||||
...messagePayload,
|
||||
@@ -921,6 +943,8 @@ export default {
|
||||
this.hideContentTemplatesModal();
|
||||
},
|
||||
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
|
||||
if (this.isMessageCreationLocked) return;
|
||||
|
||||
// Clear attachments when switching between private note and reply modes
|
||||
// This is to prevent from breaking the upload rules
|
||||
if (this.attachedFiles.length > 0) this.attachedFiles = [];
|
||||
@@ -943,6 +967,8 @@ export default {
|
||||
this.onFocus();
|
||||
},
|
||||
executeCopilotAction(action, data) {
|
||||
if (this.isMessageCreationLocked) return;
|
||||
|
||||
this.copilot.execute(action, data);
|
||||
},
|
||||
clearMessage() {
|
||||
@@ -1235,6 +1261,8 @@ export default {
|
||||
this.$nextTick(() => this.messageEditor?.focusEditorInputField());
|
||||
},
|
||||
onSubmitCopilotReply() {
|
||||
if (this.isMessageCreationLocked) return;
|
||||
|
||||
const acceptedMessage = this.copilot.accept();
|
||||
this.message = acceptedMessage;
|
||||
this.setCopilotAcceptedMessage(acceptedMessage);
|
||||
@@ -1315,6 +1343,7 @@ export default {
|
||||
v-if="copilot.isActive.value && !showAudioRecorderEditor"
|
||||
:show-copilot-editor="copilot.showEditor.value"
|
||||
:is-generating-content="copilot.isGenerating.value"
|
||||
:is-message-creation-locked="isMessageCreationLocked"
|
||||
:generated-content="copilot.generatedContent.value"
|
||||
:placeholder="$t('CONVERSATION.FOOTER.COPILOT_MSG_INPUT')"
|
||||
@focus="onFocus"
|
||||
@@ -1395,7 +1424,9 @@ export default {
|
||||
<CopilotReplyBottomPanel
|
||||
v-if="copilot.isActive.value"
|
||||
key="copilot-bottom-panel"
|
||||
:is-generating-content="copilot.isButtonDisabled.value"
|
||||
:is-generating-content="
|
||||
copilot.isButtonDisabled.value || isMessageCreationLocked
|
||||
"
|
||||
@submit="onSubmitCopilotReply"
|
||||
@cancel="copilot.reset"
|
||||
/>
|
||||
@@ -1412,6 +1443,7 @@ export default {
|
||||
:is-send-disabled="isReplyButtonDisabled"
|
||||
:is-note="isPrivate"
|
||||
:is-editor-disabled="isEditorDisabled"
|
||||
:is-message-creation-locked="isMessageCreationLocked"
|
||||
:on-file-upload="onFileUpload"
|
||||
:on-send="onSendReply"
|
||||
:conversation-type="conversationType"
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import CopilotEditorSection from '../CopilotEditorSection.vue';
|
||||
import MessagesView from '../MessagesView.vue';
|
||||
import ReplyBox from '../ReplyBox.vue';
|
||||
import ReplyBottomPanel from '../../WootWriter/ReplyBottomPanel.vue';
|
||||
|
||||
const mountCopilotEditorSection = props =>
|
||||
shallowMount(CopilotEditorSection, {
|
||||
props: {
|
||||
showCopilotEditor: true,
|
||||
isGeneratingContent: false,
|
||||
generatedContent: 'Suggested reply',
|
||||
...props,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
Transition: false,
|
||||
CopilotEditor: {
|
||||
template:
|
||||
'<button data-testid="copilot-editor" @click="$emit(\'send\')" />',
|
||||
},
|
||||
CaptainLoader: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('conversation message creation lock UI', () => {
|
||||
describe('MessagesView', () => {
|
||||
it('returns the message-limit banner copy with the configured limit', () => {
|
||||
const $t = vi.fn((key, params) => `${key}:${params.limit}`);
|
||||
const message =
|
||||
MessagesView.computed.messageCreationLockBannerMessage.call({
|
||||
currentChat: {
|
||||
message_creation_locked: true,
|
||||
message_creation_lock_reason: 'message_limit',
|
||||
message_limit: 10000,
|
||||
},
|
||||
$t,
|
||||
});
|
||||
|
||||
expect(message).toBe(
|
||||
'CONVERSATION.MESSAGE_CREATION_LOCK.MESSAGE_LIMIT:10000'
|
||||
);
|
||||
expect($t).toHaveBeenCalledWith(
|
||||
'CONVERSATION.MESSAGE_CREATION_LOCK.MESSAGE_LIMIT',
|
||||
{ limit: 10000 }
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the manual lock banner copy', () => {
|
||||
const $t = vi.fn(key => key);
|
||||
const message =
|
||||
MessagesView.computed.messageCreationLockBannerMessage.call({
|
||||
currentChat: {
|
||||
message_creation_locked: true,
|
||||
message_creation_lock_reason: 'manual',
|
||||
},
|
||||
$t,
|
||||
});
|
||||
|
||||
expect(message).toBe('CONVERSATION.MESSAGE_CREATION_LOCK.MANUAL');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReplyBox', () => {
|
||||
it('disables the editor when message creation is locked', () => {
|
||||
const isDisabled = ReplyBox.computed.isEditorDisabled.call({
|
||||
isMessageCreationLocked: true,
|
||||
isAWhatsAppChannel: false,
|
||||
isAPIInbox: false,
|
||||
isOnPrivateNote: false,
|
||||
currentChat: { can_reply: true },
|
||||
});
|
||||
|
||||
expect(isDisabled).toBe(true);
|
||||
});
|
||||
|
||||
it('uses the locked placeholder when message creation is locked', () => {
|
||||
const placeholder = ReplyBox.computed.messagePlaceHolder.call({
|
||||
isEditorDisabled: true,
|
||||
isMessageCreationLocked: true,
|
||||
$t: key => key,
|
||||
});
|
||||
|
||||
expect(placeholder).toBe('CONVERSATION.FOOTER.MESSAGE_CREATION_LOCKED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReplyBottomPanel', () => {
|
||||
it('keeps template actions available when only the editor is disabled', () => {
|
||||
const showWhatsAppTemplateButton =
|
||||
ReplyBottomPanel.computed.showWhatsAppTemplateButton.call({
|
||||
enableWhatsAppTemplates: true,
|
||||
isMessageCreationLocked: false,
|
||||
});
|
||||
const showContentTemplateButton =
|
||||
ReplyBottomPanel.computed.showContentTemplateButton.call({
|
||||
enableContentTemplates: true,
|
||||
isMessageCreationLocked: false,
|
||||
});
|
||||
|
||||
expect(showWhatsAppTemplateButton).toBe(true);
|
||||
expect(showContentTemplateButton).toBe(true);
|
||||
});
|
||||
|
||||
it('hides template actions when message creation is locked', () => {
|
||||
const showWhatsAppTemplateButton =
|
||||
ReplyBottomPanel.computed.showWhatsAppTemplateButton.call({
|
||||
enableWhatsAppTemplates: true,
|
||||
isMessageCreationLocked: true,
|
||||
});
|
||||
const showContentTemplateButton =
|
||||
ReplyBottomPanel.computed.showContentTemplateButton.call({
|
||||
enableContentTemplates: true,
|
||||
isMessageCreationLocked: true,
|
||||
});
|
||||
|
||||
expect(showWhatsAppTemplateButton).toBe(false);
|
||||
expect(showContentTemplateButton).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CopilotEditorSection', () => {
|
||||
it('keeps the follow-up editor available when message creation is unlocked', async () => {
|
||||
const wrapper = mountCopilotEditorSection({
|
||||
isMessageCreationLocked: false,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find('[data-testid="copilot-editor"]').exists()).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('hides the follow-up editor when message creation is locked', async () => {
|
||||
const wrapper = mountCopilotEditorSection({
|
||||
isMessageCreationLocked: true,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find('[data-testid="copilot-editor"]').exists()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user