Merge branch 'develop' into design-updates

This commit is contained in:
Pranav
2024-11-20 20:59:22 -08:00
6 changed files with 135 additions and 48 deletions
@@ -0,0 +1,17 @@
<script setup>
import { useChannelIcon } from './provider';
import Icon from 'next/icon/Icon.vue';
const props = defineProps({
inbox: {
type: Object,
required: true,
},
});
const channelIcon = useChannelIcon(props.inbox);
</script>
<template>
<Icon :icon="channelIcon" />
</template>
@@ -0,0 +1,36 @@
import { computed } from 'vue';
export function useChannelIcon(inbox) {
const channelTypeIconMap = {
'Channel::Api': 'i-ri-cloudy-fill',
'Channel::Email': 'i-ri-mail-fill',
'Channel::FacebookPage': 'i-ri-messenger-fill',
'Channel::Line': 'i-ri-line-fill',
'Channel::Sms': 'i-ri-chat-1-fill',
'Channel::Telegram': 'i-ri-telegram-fill',
'Channel::TwilioSms': 'i-ri-chat-1-fill',
'Channel::TwitterProfile': 'i-ri-twitter-x-fill',
'Channel::WebWidget': 'i-ri-global-fill',
'Channel::Whatsapp': 'i-ri-whatsapp-fill',
};
const providerIconMap = {
microsoft: 'i-ri-microsoft-fill',
google: 'i-ri-google-fill',
};
const channelIcon = computed(() => {
const type = inbox.channel_type;
let icon = channelTypeIconMap[type];
if (type === 'Channel::Email' && inbox.provider) {
if (Object.keys(providerIconMap).includes(inbox.provider)) {
icon = providerIconMap[inbox.provider];
}
}
return icon ?? 'i-ri-global-fill';
});
return channelIcon;
}
@@ -0,0 +1,59 @@
import { useChannelIcon } from '../provider';
describe('useChannelIcon', () => {
it('returns correct icon for API channel', () => {
const inbox = { channel_type: 'Channel::Api' };
const { value: icon } = useChannelIcon(inbox);
expect(icon).toBe('i-ri-cloudy-fill');
});
it('returns correct icon for Facebook channel', () => {
const inbox = { channel_type: 'Channel::FacebookPage' };
const { value: icon } = useChannelIcon(inbox);
expect(icon).toBe('i-ri-messenger-fill');
});
it('returns correct icon for WhatsApp channel', () => {
const inbox = { channel_type: 'Channel::Whatsapp' };
const { value: icon } = useChannelIcon(inbox);
expect(icon).toBe('i-ri-whatsapp-fill');
});
describe('Email channel', () => {
it('returns mail icon for generic email channel', () => {
const inbox = { channel_type: 'Channel::Email' };
const { value: icon } = useChannelIcon(inbox);
expect(icon).toBe('i-ri-mail-fill');
});
it('returns Microsoft icon for Microsoft email provider', () => {
const inbox = {
channel_type: 'Channel::Email',
provider: 'microsoft',
};
const { value: icon } = useChannelIcon(inbox);
expect(icon).toBe('i-ri-microsoft-fill');
});
it('returns Google icon for Google email provider', () => {
const inbox = {
channel_type: 'Channel::Email',
provider: 'google',
};
const { value: icon } = useChannelIcon(inbox);
expect(icon).toBe('i-ri-google-fill');
});
});
it('returns default icon for unknown channel type', () => {
const inbox = { channel_type: 'Channel::Unknown' };
const { value: icon } = useChannelIcon(inbox);
expect(icon).toBe('i-ri-global-fill');
});
it('returns default icon when channel type is undefined', () => {
const inbox = {};
const { value: icon } = useChannelIcon(inbox);
expect(icon).toBe('i-ri-global-fill');
});
});
@@ -1,6 +1,7 @@
<script setup>
import { computed } from 'vue';
import Icon from 'next/icon/Icon.vue';
import ChannelIcon from 'next/icon/ChannelIcon.vue';
const props = defineProps({
label: {
@@ -17,37 +18,6 @@ const props = defineProps({
},
});
const channelTypeIconMap = {
'Channel::Api': 'i-ri-cloudy-fill',
'Channel::Email': 'i-ri-mail-fill',
'Channel::FacebookPage': 'i-ri-messenger-fill',
'Channel::Line': 'i-ri-line-fill',
'Channel::Sms': 'i-ri-chat-1-fill',
'Channel::Telegram': 'i-ri-telegram-fill',
'Channel::TwilioSms': 'i-ri-chat-1-fill',
'Channel::TwitterProfile': 'i-ri-twitter-x-fill',
'Channel::WebWidget': 'i-ri-global-fill',
'Channel::Whatsapp': 'i-ri-whatsapp-fill',
};
const providerIconMap = {
microsoft: 'i-ri-microsoft-fill',
google: 'i-ri-google-fill',
};
const channelIcon = computed(() => {
const type = props.inbox.channel_type;
let icon = channelTypeIconMap[type];
if (type === 'Channel::Email' && props.inbox.provider) {
if (Object.keys(providerIconMap).includes(props.inbox.provider)) {
icon = providerIconMap[props.inbox.provider];
}
}
return icon ?? 'i-ri-global-fill';
});
const reauthorizationRequired = computed(() => {
return props.inbox.reauthorization_required;
});
@@ -58,7 +28,7 @@ const reauthorizationRequired = computed(() => {
class="size-4 grid place-content-center rounded-full bg-n-alpha-2"
:class="{ 'bg-n-solid-blue': active }"
>
<Icon :icon="channelIcon" class="size-3" />
<ChannelIcon :inbox="inbox" class="size-3" />
</span>
<div class="flex-1 truncate min-w-0">{{ label }}</div>
<div
+5 -8
View File
@@ -8,12 +8,12 @@ class ApplicationMailbox < ActionMailbox::Base
# routes as a reply to existing conversations
routing(
->(inbound_mail) { reply_uuid_mail?(inbound_mail) || in_reply_to_mail?(inbound_mail) } => :reply
->(inbound_mail) { valid_to_address?(inbound_mail) && (reply_uuid_mail?(inbound_mail) || in_reply_to_mail?(inbound_mail)) } => :reply
)
# routes as a new conversation in email channel
routing(
->(inbound_mail) { EmailChannelFinder.new(inbound_mail.mail).perform.present? } => :support
->(inbound_mail) { valid_to_address?(inbound_mail) && EmailChannelFinder.new(inbound_mail.mail).perform.present? } => :support
)
# catchall
@@ -37,8 +37,6 @@ class ApplicationMailbox < ActionMailbox::Base
# checks if follow this pattern send it to reply_mailbox
# reply+<conversation-uuid>@<mailer-domain.com>
def reply_uuid_mail?(inbound_mail)
validate_to_address(inbound_mail)
inbound_mail.mail.to&.any? do |email|
conversation_uuid = email.split('@')[0]
conversation_uuid.match?(REPLY_EMAIL_UUID_PATTERN)
@@ -48,13 +46,12 @@ class ApplicationMailbox < ActionMailbox::Base
# if mail.to returns a string, then it is a malformed `to` header
# valid `to` header will be of type Mail::AddressContainer
# validate if the to address is of type string
def validate_to_address(inbound_mail)
def valid_to_address?(inbound_mail)
to_address_class = inbound_mail.mail.to&.class
return if to_address_class == Mail::AddressContainer
return true if to_address_class == Mail::AddressContainer
Rails.logger.error "Email to address header is malformed `#{inbound_mail.mail.to}`"
raise StandardError, "Invalid email to address header #{inbound_mail.mail.to}"
false
end
end
end
+16 -8
View File
@@ -69,18 +69,26 @@ RSpec.describe ApplicationMailbox do
end
describe 'Invalid Mail To Address' do
it 'raises error when mail.to header is malformed' do
expect do
described_class.route mail_with_invalid_to_address
end.to raise_error(StandardError,
'Invalid email to address header <vishnu@chatwoot.com>vishnu@chatwoot.com')
let(:logger) { double }
before do
allow(Rails).to receive(:logger).and_return(logger)
allow(logger).to receive(:error)
end
it 'raises another error when mail.to header is malformed' do
it 'will not raise error when mail.to header is malformed format 1' do
expect(logger).to receive(:error).with("Email to address header is malformed `#{mail_with_invalid_to_address.mail.to}`")
expect do
described_class.route mail_with_invalid_to_address
end.not_to raise_error
end
it 'will not raise error when mail.to header is malformed format 2' do
expect(logger).to receive(:error).with("Email to address header is malformed `#{mail_with_invalid_to_address_2.mail.to}`")
expect do
described_class.route mail_with_invalid_to_address_2
end.to raise_error(StandardError,
'Invalid email to address header vishnu@chatwoot.com www.chatwoot.com')
end.not_to raise_error
end
end
end