From b919afa182c7c479b6b8f2d0e48125c5fa680d03 Mon Sep 17 00:00:00 2001 From: iamsivin Date: Thu, 21 Nov 2024 00:53:45 +0530 Subject: [PATCH 1/5] chore: Minor fix --- .../dashboard/components-next/Contacts/Pages/ContactsList.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/javascript/dashboard/components-next/Contacts/Pages/ContactsList.vue b/app/javascript/dashboard/components-next/Contacts/Pages/ContactsList.vue index 8e94884bd..3be194847 100644 --- a/app/javascript/dashboard/components-next/Contacts/Pages/ContactsList.vue +++ b/app/javascript/dashboard/components-next/Contacts/Pages/ContactsList.vue @@ -37,8 +37,6 @@ const ROUTE_MAPPINGS = { }; const onClickViewDetails = async id => { - await store.dispatch('contacts/show', { id }); - const dynamicRouteName = ROUTE_MAPPINGS[route.name] || 'contacts_dashboard_edit_index'; From 515778eabbb6a941962dc04b96759949aa165aae Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 21 Nov 2024 10:58:54 +0800 Subject: [PATCH 2/5] chore: Disable throwing error for malformed to address (#10464) We don't need to raise error on sentry for malformed to address as it is already logged. Fixes: https://linear.app/chatwoot/issue/CW-3151/standarderror-invalid-email-to-address-header-standarderror --- app/mailboxes/application_mailbox.rb | 13 +++++------- spec/mailboxes/application_mailbox_spec.rb | 24 ++++++++++++++-------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app/mailboxes/application_mailbox.rb b/app/mailboxes/application_mailbox.rb index 6a1902ce4..4fe931b9f 100644 --- a/app/mailboxes/application_mailbox.rb +++ b/app/mailboxes/application_mailbox.rb @@ -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+@ 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 diff --git a/spec/mailboxes/application_mailbox_spec.rb b/spec/mailboxes/application_mailbox_spec.rb index 733ec5523..f4f28d811 100644 --- a/spec/mailboxes/application_mailbox_spec.rb +++ b/spec/mailboxes/application_mailbox_spec.rb @@ -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') + 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 From 3a334be58214db8017d629937f45c680d351fe5a Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 21 Nov 2024 09:53:12 +0530 Subject: [PATCH 3/5] feat: add channel icon component (#10471) This pull request introduces a new `ChannelIcon` component and refactors the existing code to use this component, which simplifies the icon management for different channel types and providers. --- .../components-next/icon/ChannelIcon.vue | 17 ++++++ .../components-next/icon/provider.js | 36 +++++++++++ .../icon/specs/provider.spec.js | 59 +++++++++++++++++++ .../components-next/sidebar/ChannelLeaf.vue | 34 +---------- 4 files changed, 114 insertions(+), 32 deletions(-) create mode 100644 app/javascript/dashboard/components-next/icon/ChannelIcon.vue create mode 100644 app/javascript/dashboard/components-next/icon/provider.js create mode 100644 app/javascript/dashboard/components-next/icon/specs/provider.spec.js diff --git a/app/javascript/dashboard/components-next/icon/ChannelIcon.vue b/app/javascript/dashboard/components-next/icon/ChannelIcon.vue new file mode 100644 index 000000000..11117cc18 --- /dev/null +++ b/app/javascript/dashboard/components-next/icon/ChannelIcon.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/javascript/dashboard/components-next/icon/provider.js b/app/javascript/dashboard/components-next/icon/provider.js new file mode 100644 index 000000000..5b00376d1 --- /dev/null +++ b/app/javascript/dashboard/components-next/icon/provider.js @@ -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; +} diff --git a/app/javascript/dashboard/components-next/icon/specs/provider.spec.js b/app/javascript/dashboard/components-next/icon/specs/provider.spec.js new file mode 100644 index 000000000..df30d7138 --- /dev/null +++ b/app/javascript/dashboard/components-next/icon/specs/provider.spec.js @@ -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'); + }); +}); diff --git a/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue b/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue index e3a2430a5..2a93089a9 100644 --- a/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue +++ b/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue @@ -1,6 +1,7 @@ + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsCard/story/ContactsCard.story.vue b/app/javascript/dashboard/components-next/Contacts/ContactsCard/story/ContactsCard.story.vue new file mode 100644 index 000000000..3f8d5a411 --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsCard/story/ContactsCard.story.vue @@ -0,0 +1,67 @@ + + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsCard/story/fixtures.js b/app/javascript/dashboard/components-next/Contacts/ContactsCard/story/fixtures.js new file mode 100644 index 000000000..67bdba9b4 --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsCard/story/fixtures.js @@ -0,0 +1,149 @@ +export default [ + { + additionalAttributes: { + socialProfiles: {}, + }, + availabilityStatus: null, + email: 'johndoe@chatwoot.com', + id: 370, + name: 'John Doe', + phoneNumber: '+918634322418', + identifier: null, + thumbnail: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Felix', + customAttributes: {}, + lastActivityAt: 1731608270, + createdAt: 1731586271, + }, + { + additionalAttributes: { + city: 'kerala', + country: 'India', + description: 'Curious about the web. ', + companyName: 'Chatwoot', + countryCode: '', + socialProfiles: { + github: 'abozler', + twitter: 'ozler', + facebook: 'abozler', + linkedin: 'abozler', + instagram: 'ozler', + }, + }, + availabilityStatus: null, + email: 'ozler@chatwoot.com', + id: 29, + name: 'Abraham Ozlers', + phoneNumber: '+246232222222', + identifier: null, + thumbnail: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Upload', + customAttributes: { + dateContact: '2024-02-01T00:00:00.000Z', + linkContact: 'https://staging.chatwoot.com/app/accounts/3/contacts-new', + listContact: 'Not spam', + numberContact: '12', + }, + lastActivityAt: 1712127410, + createdAt: 1712127389, + }, + { + additionalAttributes: { + city: 'Kerala', + country: 'India', + description: + "I'm Candice developer focusing on building things for the web 🌍. Currently, I’m working as a Product Developer here at @chatwootapp ⚡️🔥", + companyName: 'Chatwoot', + countryCode: 'IN', + socialProfiles: { + github: 'cmathersonj', + twitter: 'cmather', + facebook: 'cmathersonj', + linkedin: 'cmathersonj', + instagram: 'cmathersonjs', + }, + }, + availabilityStatus: null, + email: 'cmathersonj@va.test', + id: 22, + name: 'Candice Matherson', + phoneNumber: '+917474774742', + identifier: null, + thumbnail: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Emery', + customAttributes: { + dateContact: '2024-11-12T03:23:06.963Z', + linkContact: 'https://sd.sd', + textContact: 'hey', + numberContact: '12', + checkboxContact: true, + }, + lastActivityAt: 1712123233, + createdAt: 1712123233, + }, + { + additionalAttributes: { + city: '', + country: '', + description: '', + companyName: '', + countryCode: '', + socialProfiles: { + github: '', + twitter: '', + facebook: '', + linkedin: '', + instagram: '', + }, + }, + availabilityStatus: null, + email: 'ofolkardi@taobao.test', + id: 21, + name: 'Ophelia Folkard', + phoneNumber: '', + identifier: null, + thumbnail: + 'https://sivin-tunnel.chatwoot.dev/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBPZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--08dcac8eb72ef12b2cad92d58dddd04cd8a5f513/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RTNKbGMybDZaVjkwYjE5bWFXeHNXd2RwQWZvdyIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--df796c2af3c0153e55236c2f3cf3a199ac2cb6f7/32.jpg', + customAttributes: {}, + lastActivityAt: 1712123233, + createdAt: 1712123233, + }, + { + additionalAttributes: { + socialProfiles: {}, + }, + availabilityStatus: null, + email: 'wcasteloth@exblog.jp', + id: 20, + name: 'Willy Castelot', + phoneNumber: '+919384', + identifier: null, + thumbnail: 'https://api.dicebear.com/9.x/thumbs/svg?seed=Jade', + customAttributes: {}, + lastActivityAt: 1712123233, + createdAt: 1712123233, + }, + { + additionalAttributes: { + city: '', + country: '', + description: '', + companyName: '', + countryCode: '', + socialProfiles: { + github: '', + twitter: '', + facebook: '', + linkedin: '', + instagram: '', + }, + }, + availabilityStatus: null, + email: 'ederingtong@printfriendly.test', + id: 19, + name: 'Elisabeth Derington', + phoneNumber: '', + identifier: null, + thumbnail: 'https://api.dicebear.com/9.x/avataaars/svg?seed=Jade', + customAttributes: {}, + lastActivityAt: 1712123232, + createdAt: 1712123232, + }, +]; diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue new file mode 100644 index 000000000..866811ae2 --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue @@ -0,0 +1,297 @@ + + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/ContactsForm.story.vue b/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/ContactsForm.story.vue new file mode 100644 index 000000000..4be69380c --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/ContactsForm.story.vue @@ -0,0 +1,80 @@ + + + diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/fixtures.js b/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/fixtures.js new file mode 100644 index 000000000..30eb6ee22 --- /dev/null +++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/story/fixtures.js @@ -0,0 +1,20 @@ +export default { + id: 370, + name: 'John Doe', + email: 'johndoe@chatwoot.com', + phoneNumber: '+918634322418', + additionalAttributes: { + city: 'Kerala', + country: 'India', + description: 'Curious about the web.', + companyName: 'Chatwoot', + countryCode: 'IN', + socialProfiles: { + github: 'johndoe', + twitter: 'johndoe', + facebook: 'johndoe', + linkedin: 'johndoe', + instagram: 'johndoe', + }, + }, +}; diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index 54a06bb53..e0483d224 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -385,5 +385,61 @@ "DROPDOWN_ITEM": { "ID": "(ID: {identifier})" } + }, + + "CONTACTS_LAYOUT": { + "CARD": { + "OF": "of", + "VIEW_DETAILS": "View details", + "EDIT_DETAILS_FORM": { + "TITLE": "Edit contact details", + "FORM": { + "FIRST_NAME": { + "PLACEHOLDER": "Enter the first name" + }, + "LAST_NAME": { + "PLACEHOLDER": "Enter the last name" + }, + "EMAIL_ADDRESS": { + "PLACEHOLDER": "Enter the email address" + }, + "PHONE_NUMBER": { + "PLACEHOLDER": "Enter the phone number" + }, + "CITY": { + "PLACEHOLDER": "Enter the city name" + }, + "COUNTRY": { + "PLACEHOLDER": "Select country" + }, + "BIO": { + "PLACEHOLDER": "Enter the bio" + }, + "COMPANY_NAME": { + "PLACEHOLDER": "Enter the company name" + } + } + }, + "SOCIAL_MEDIA": { + "TITLE": "Edit social links", + "FORM": { + "FACEBOOK": { + "PLACEHOLDER": "Add Facebook" + }, + "GITHUB": { + "PLACEHOLDER": "Add Github" + }, + "INSTAGRAM": { + "PLACEHOLDER": "Add Instagram" + }, + "LINKEDIN": { + "PLACEHOLDER": "Add LinkedIn" + }, + "TWITTER": { + "PLACEHOLDER": "Add Twitter" + } + } + } + } } }