feat: Support cc and bcc in email replies (#3098)
Co-authored-by: Tejaswini <tejaswini@chatwoot.com> Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
0e0632be22
commit
68e697c379
8 changed files with 135 additions and 25 deletions
|
@ -16,6 +16,7 @@ class Messages::MessageBuilder
|
||||||
def perform
|
def perform
|
||||||
@message = @conversation.messages.build(message_params)
|
@message = @conversation.messages.build(message_params)
|
||||||
process_attachments
|
process_attachments
|
||||||
|
process_emails
|
||||||
@message.save!
|
@message.save!
|
||||||
@message
|
@message
|
||||||
end
|
end
|
||||||
|
@ -34,6 +35,16 @@ class Messages::MessageBuilder
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_emails
|
||||||
|
return unless @conversation.inbox&.inbox_type == 'Email'
|
||||||
|
|
||||||
|
cc_emails = @params[:cc_emails].split(',') if @params[:cc_emails]
|
||||||
|
bcc_emails = @params[:bcc_emails].split(',') if @params[:bcc_emails]
|
||||||
|
|
||||||
|
@message.content_attributes[:cc_emails] = cc_emails
|
||||||
|
@message.content_attributes[:bcc_emails] = bcc_emails
|
||||||
|
end
|
||||||
|
|
||||||
def message_type
|
def message_type
|
||||||
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
|
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
|
||||||
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
|
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
|
||||||
|
|
|
@ -8,6 +8,8 @@ export const buildCreatePayload = ({
|
||||||
contentAttributes,
|
contentAttributes,
|
||||||
echoId,
|
echoId,
|
||||||
file,
|
file,
|
||||||
|
ccEmails,
|
||||||
|
bccEmails,
|
||||||
}) => {
|
}) => {
|
||||||
let payload;
|
let payload;
|
||||||
if (file) {
|
if (file) {
|
||||||
|
@ -18,12 +20,16 @@ export const buildCreatePayload = ({
|
||||||
}
|
}
|
||||||
payload.append('private', isPrivate);
|
payload.append('private', isPrivate);
|
||||||
payload.append('echo_id', echoId);
|
payload.append('echo_id', echoId);
|
||||||
|
payload.append('cc_emails', ccEmails);
|
||||||
|
payload.append('bcc_emails', bccEmails);
|
||||||
} else {
|
} else {
|
||||||
payload = {
|
payload = {
|
||||||
content: message,
|
content: message,
|
||||||
private: isPrivate,
|
private: isPrivate,
|
||||||
echo_id: echoId,
|
echo_id: echoId,
|
||||||
content_attributes: contentAttributes,
|
content_attributes: contentAttributes,
|
||||||
|
cc_emails: ccEmails,
|
||||||
|
bcc_emails: bccEmails,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
|
@ -41,6 +47,8 @@ class MessageApi extends ApiClient {
|
||||||
contentAttributes,
|
contentAttributes,
|
||||||
echo_id: echoId,
|
echo_id: echoId,
|
||||||
file,
|
file,
|
||||||
|
ccEmails,
|
||||||
|
bccEmails,
|
||||||
}) {
|
}) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
@ -51,6 +59,8 @@ class MessageApi extends ApiClient {
|
||||||
contentAttributes,
|
contentAttributes,
|
||||||
echoId,
|
echoId,
|
||||||
file,
|
file,
|
||||||
|
ccEmails,
|
||||||
|
bccEmails,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
<div :class="wrapClass">
|
<div :class="wrapClass">
|
||||||
<div v-tooltip.top-start="sentByMessage" :class="bubbleClass">
|
<div v-tooltip.top-start="sentByMessage" :class="bubbleClass">
|
||||||
<bubble-mail-head
|
<bubble-mail-head
|
||||||
v-if="isEmailContentType"
|
|
||||||
:email-attributes="contentAttributes.email"
|
:email-attributes="contentAttributes.email"
|
||||||
|
:cc="emailHeadAttributes.cc"
|
||||||
|
:bcc="emailHeadAttributes.bcc"
|
||||||
:is-incoming="isIncoming"
|
:is-incoming="isIncoming"
|
||||||
/>
|
/>
|
||||||
<bubble-text
|
<bubble-text
|
||||||
|
@ -222,6 +223,13 @@ export default {
|
||||||
isIncoming() {
|
isIncoming() {
|
||||||
return this.data.message_type === MESSAGE_TYPE.INCOMING;
|
return this.data.message_type === MESSAGE_TYPE.INCOMING;
|
||||||
},
|
},
|
||||||
|
emailHeadAttributes() {
|
||||||
|
return {
|
||||||
|
email: this.contentAttributes.email,
|
||||||
|
cc: this.contentAttributes.cc_emails,
|
||||||
|
bcc: this.contentAttributes.bcc_emails
|
||||||
|
}
|
||||||
|
},
|
||||||
hasAttachments() {
|
hasAttachments() {
|
||||||
return !!(this.data.attachments && this.data.attachments.length > 0);
|
return !!(this.data.attachments && this.data.attachments.length > 0);
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,6 +20,11 @@
|
||||||
v-on-clickaway="hideEmojiPicker"
|
v-on-clickaway="hideEmojiPicker"
|
||||||
:on-click="emojiOnClick"
|
:on-click="emojiOnClick"
|
||||||
/>
|
/>
|
||||||
|
<reply-email-head
|
||||||
|
v-if="showReplyHead"
|
||||||
|
@set-emails="setCcEmails"
|
||||||
|
:clear-mails="clearMails"
|
||||||
|
/>
|
||||||
<resizable-text-area
|
<resizable-text-area
|
||||||
v-if="!showRichContentEditor"
|
v-if="!showRichContentEditor"
|
||||||
ref="messageInput"
|
ref="messageInput"
|
||||||
|
@ -82,6 +87,7 @@ import CannedResponse from './CannedResponse';
|
||||||
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
||||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
|
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
|
||||||
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel';
|
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel';
|
||||||
|
import ReplyEmailHead from './ReplyEmailHead';
|
||||||
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel';
|
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel';
|
||||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||||
|
@ -104,6 +110,7 @@ export default {
|
||||||
ResizableTextArea,
|
ResizableTextArea,
|
||||||
AttachmentPreview,
|
AttachmentPreview,
|
||||||
ReplyTopPanel,
|
ReplyTopPanel,
|
||||||
|
ReplyEmailHead,
|
||||||
ReplyBottomPanel,
|
ReplyBottomPanel,
|
||||||
WootMessageEditor,
|
WootMessageEditor,
|
||||||
},
|
},
|
||||||
|
@ -134,6 +141,7 @@ export default {
|
||||||
mentionSearchKey: '',
|
mentionSearchKey: '',
|
||||||
hasUserMention: false,
|
hasUserMention: false,
|
||||||
hasSlashCommand: false,
|
hasSlashCommand: false,
|
||||||
|
clearMails: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -270,6 +278,9 @@ export default {
|
||||||
}
|
}
|
||||||
return !this.message.trim().replace(/\n/g, '').length;
|
return !this.message.trim().replace(/\n/g, '').length;
|
||||||
},
|
},
|
||||||
|
showReplyHead(){
|
||||||
|
return this.isAnEmailChannel;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentChat(conversation) {
|
currentChat(conversation) {
|
||||||
|
@ -354,6 +365,7 @@ export default {
|
||||||
this.showAlert(errorMessage);
|
this.showAlert(errorMessage);
|
||||||
}
|
}
|
||||||
this.hideEmojiPicker();
|
this.hideEmojiPicker();
|
||||||
|
this.clearMails = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
replaceText(message) {
|
replaceText(message) {
|
||||||
|
@ -376,6 +388,7 @@ export default {
|
||||||
clearMessage() {
|
clearMessage() {
|
||||||
this.message = '';
|
this.message = '';
|
||||||
this.attachedFiles = [];
|
this.attachedFiles = [];
|
||||||
|
this.clearMails = true;
|
||||||
},
|
},
|
||||||
toggleEmojiPicker() {
|
toggleEmojiPicker() {
|
||||||
this.showEmojiPicker = !this.showEmojiPicker;
|
this.showEmojiPicker = !this.showEmojiPicker;
|
||||||
|
@ -452,11 +465,23 @@ export default {
|
||||||
messagePayload.file = attachment.resource.file;
|
messagePayload.file = attachment.resource.file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(this.ccEmails) {
|
||||||
|
messagePayload.ccEmails = this.ccEmails;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.bccEmails) {
|
||||||
|
messagePayload.bccEmails = this.bccEmails;
|
||||||
|
}
|
||||||
|
|
||||||
return messagePayload;
|
return messagePayload;
|
||||||
},
|
},
|
||||||
setFormatMode(value) {
|
setFormatMode(value) {
|
||||||
this.updateUISettings({ display_rich_content_editor: value });
|
this.updateUISettings({ display_rich_content_editor: value });
|
||||||
},
|
},
|
||||||
|
setCcEmails(value) {
|
||||||
|
this.bccEmails = value.bccEmails;
|
||||||
|
this.ccEmails = value.ccEmails
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="input-group-wrap">
|
<div class="input-group-wrap">
|
||||||
<div class="input-group small" :class="{ error: $v.ccEmails.$error }">
|
<div class="input-group small" :class="{ error: $v.ccEmailsVal.$error }">
|
||||||
<label class="input-group-label">
|
<label class="input-group-label">
|
||||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.LABEL') }}
|
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.LABEL') }}
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group-field">
|
<div class="input-group-field">
|
||||||
<woot-input
|
<woot-input
|
||||||
v-model.trim="ccEmails"
|
v-model.trim="$v.ccEmailsVal.$model"
|
||||||
type="email"
|
type="email"
|
||||||
:class="{ error: $v.ccEmails.$error }"
|
:class="{ error: $v.ccEmailsVal.$error }"
|
||||||
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
|
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
|
||||||
@blur="$v.ccEmails.$touch"
|
@blur="onBlur"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<woot-button
|
<woot-button
|
||||||
|
@ -23,28 +23,28 @@
|
||||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.ADD_BCC') }}
|
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.ADD_BCC') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="$v.ccEmails.$error" class="message">
|
<span v-if="$v.ccEmailsVal.$error" class="message">
|
||||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.ERROR') }}
|
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.ERROR') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showBcc" class="input-group-wrap">
|
<div v-if="showBcc" class="input-group-wrap">
|
||||||
<div class="input-group small" :class="{ error: $v.bccEmails.$error }">
|
<div class="input-group small" :class="{ error: $v.bccEmailsVal.$error }">
|
||||||
<label class="input-group-label">
|
<label class="input-group-label">
|
||||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.LABEL') }}
|
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.LABEL') }}
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group-field">
|
<div class="input-group-field">
|
||||||
<woot-input
|
<woot-input
|
||||||
v-model.trim="bccEmails"
|
v-model.trim="$v.bccEmailsVal.$model"
|
||||||
type="email"
|
type="email"
|
||||||
:class="{ error: $v.bccEmails.$error }"
|
:class="{ error: $v.bccEmailsVal.$error }"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER')
|
$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER')
|
||||||
"
|
"
|
||||||
@blur="$v.bccEmails.$touch"
|
@blur="onBlur"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="$v.bccEmails.$error" class="message">
|
<span v-if="$v.bccEmailsVal.$error" class="message">
|
||||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.ERROR') }}
|
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.ERROR') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,27 +55,25 @@
|
||||||
import { validEmailsByComma } from './helpers/emailHeadHelper';
|
import { validEmailsByComma } from './helpers/emailHeadHelper';
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
ccEmails: {
|
clearMails: {
|
||||||
type: String,
|
type: Boolean,
|
||||||
default: '',
|
default: false,
|
||||||
},
|
|
||||||
bccEmails: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showBcc: false,
|
showBcc: false,
|
||||||
|
ccEmailsVal: '',
|
||||||
|
bccEmailsVal: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
validations: {
|
validations: {
|
||||||
ccEmails: {
|
ccEmailsVal: {
|
||||||
hasValidEmails(value) {
|
hasValidEmails(value) {
|
||||||
return validEmailsByComma(value);
|
return validEmailsByComma(value);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bccEmails: {
|
bccEmailsVal: {
|
||||||
hasValidEmails(value) {
|
hasValidEmails(value) {
|
||||||
return validEmailsByComma(value);
|
return validEmailsByComma(value);
|
||||||
},
|
},
|
||||||
|
@ -85,7 +83,20 @@ export default {
|
||||||
handleAddBcc() {
|
handleAddBcc() {
|
||||||
this.showBcc = true;
|
this.showBcc = true;
|
||||||
},
|
},
|
||||||
|
onBlur() {
|
||||||
|
this.$v.$touch();
|
||||||
|
this.$emit("set-emails", { bccEmails: this.bccEmailsVal, ccEmails: this.ccEmailsVal });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
clearMails: function(value){
|
||||||
|
if(value) {
|
||||||
|
this.ccEmailsVal = '';
|
||||||
|
this.bccEmailsVal = '';
|
||||||
|
this.clearMails = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -36,6 +36,14 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
cc: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
bcc: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
toMails() {
|
toMails() {
|
||||||
|
@ -43,11 +51,11 @@ export default {
|
||||||
return to.join(', ');
|
return to.join(', ');
|
||||||
},
|
},
|
||||||
ccMails() {
|
ccMails() {
|
||||||
const cc = this.emailAttributes.cc || [];
|
const cc = this.emailAttributes.cc || this.cc || [];
|
||||||
return cc.join(', ');
|
return cc.join(', ');
|
||||||
},
|
},
|
||||||
bccMails() {
|
bccMails() {
|
||||||
const bcc = this.emailAttributes.bcc || [];
|
const bcc = this.emailAttributes.bcc || this.bcc || [];
|
||||||
return bcc.join(', ');
|
return bcc.join(', ');
|
||||||
},
|
},
|
||||||
subject() {
|
subject() {
|
||||||
|
|
|
@ -19,7 +19,9 @@ class ConversationReplyMailer < ApplicationMailer
|
||||||
reply_to: reply_email,
|
reply_to: reply_email,
|
||||||
subject: mail_subject,
|
subject: mail_subject,
|
||||||
message_id: custom_message_id,
|
message_id: custom_message_id,
|
||||||
in_reply_to: in_reply_to_email
|
in_reply_to: in_reply_to_email,
|
||||||
|
cc: cc_bcc_emails[0],
|
||||||
|
bcc: cc_bcc_emails[1]
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -39,7 +41,9 @@ class ConversationReplyMailer < ApplicationMailer
|
||||||
reply_to: reply_email,
|
reply_to: reply_email,
|
||||||
subject: mail_subject,
|
subject: mail_subject,
|
||||||
message_id: custom_message_id,
|
message_id: custom_message_id,
|
||||||
in_reply_to: in_reply_to_email
|
in_reply_to: in_reply_to_email,
|
||||||
|
cc: cc_bcc_emails[0],
|
||||||
|
bcc: cc_bcc_emails[1]
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -142,6 +146,15 @@ class ConversationReplyMailer < ApplicationMailer
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cc_bcc_emails
|
||||||
|
content_attributes = @conversation.messages.outgoing.last&.content_attributes
|
||||||
|
|
||||||
|
return [] unless content_attributes
|
||||||
|
return [] unless content_attributes[:cc_emails] || content_attributes[:bcc_emails]
|
||||||
|
|
||||||
|
[content_attributes[:cc_emails], content_attributes[:bcc_emails]]
|
||||||
|
end
|
||||||
|
|
||||||
def inbound_email_enabled?
|
def inbound_email_enabled?
|
||||||
@inbound_email_enabled ||= @account.feature_enabled?('inbound_emails') && @account.inbound_email_domain
|
@inbound_email_enabled ||= @account.feature_enabled?('inbound_emails') && @account.inbound_email_domain
|
||||||
.present? && @account.support_email.present?
|
.present? && @account.support_email.present?
|
||||||
|
|
|
@ -15,9 +15,28 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
|
||||||
|
|
||||||
context 'with summary' do
|
context 'with summary' do
|
||||||
let(:conversation) { create(:conversation, account: account, assignee: agent) }
|
let(:conversation) { create(:conversation, account: account, assignee: agent) }
|
||||||
let(:message) { create(:message, account: account, conversation: conversation) }
|
let(:message) do
|
||||||
|
create(:message,
|
||||||
|
account: account,
|
||||||
|
conversation: conversation,
|
||||||
|
content_attributes: {
|
||||||
|
cc_emails: 'agent_cc1@example.com',
|
||||||
|
bcc_emails: 'agent_bcc1@example.com'
|
||||||
|
})
|
||||||
|
end
|
||||||
|
let(:cc_message) do
|
||||||
|
create(:message,
|
||||||
|
account: account,
|
||||||
|
message_type: :outgoing,
|
||||||
|
conversation: conversation,
|
||||||
|
content_attributes: {
|
||||||
|
cc_emails: 'agent_cc1@example.com',
|
||||||
|
bcc_emails: 'agent_bcc1@example.com'
|
||||||
|
})
|
||||||
|
end
|
||||||
let(:private_message) { create(:message, account: account, content: 'This is a private message', conversation: conversation) }
|
let(:private_message) { create(:message, account: account, content: 'This is a private message', conversation: conversation) }
|
||||||
let(:mail) { described_class.reply_with_summary(message.conversation, Time.zone.now).deliver_now }
|
let(:mail) { described_class.reply_with_summary(message.conversation, Time.zone.now).deliver_now }
|
||||||
|
let(:cc_mail) { described_class.reply_with_summary(cc_message.conversation, Time.zone.now).deliver_now }
|
||||||
|
|
||||||
it 'renders the subject' do
|
it 'renders the subject' do
|
||||||
expect(mail.subject).to eq("[##{message.conversation.display_id}] New messages on this conversation")
|
expect(mail.subject).to eq("[##{message.conversation.display_id}] New messages on this conversation")
|
||||||
|
@ -37,6 +56,11 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
|
||||||
conversation.update(contact_last_seen_at: Time.zone.now)
|
conversation.update(contact_last_seen_at: Time.zone.now)
|
||||||
expect(mail).to eq nil
|
expect(mail).to eq nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'will send email to cc and bcc email addresses' do
|
||||||
|
expect(cc_mail.cc.first).to eq(cc_message.content_attributes[:cc_emails])
|
||||||
|
expect(cc_mail.bcc.first).to eq(cc_message.content_attributes[:bcc_emails])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'without assignee' do
|
context 'without assignee' do
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue