diff --git a/app/builders/agent_builder.rb b/app/builders/agent_builder.rb index 07b0e7345..54f478920 100644 --- a/app/builders/agent_builder.rb +++ b/app/builders/agent_builder.rb @@ -16,7 +16,6 @@ class AgentBuilder def perform ActiveRecord::Base.transaction do @user = find_or_create_user - send_confirmation_if_required create_account_user end @user @@ -34,11 +33,6 @@ class AgentBuilder User.create!(email: email, name: name, password: temp_password, password_confirmation: temp_password) end - # Sends confirmation instructions if the user is persisted and not confirmed. - def send_confirmation_if_required - @user.send_confirmation_instructions if user_needs_confirmation? - end - # Checks if the user needs confirmation. # @return [Boolean] true if the user is persisted and not confirmed, false otherwise. def user_needs_confirmation? diff --git a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb index d2d51baef..ebf8e49dd 100644 --- a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb +++ b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb @@ -2,13 +2,9 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts: before_action :authorize_request def create - ActiveRecord::Base.transaction do - authenticate_twilio - build_inbox - setup_webhooks if @twilio_channel.sms? - rescue StandardError => e - render_could_not_create_error(e.message) - end + process_create + rescue StandardError => e + render_could_not_create_error(e.message) end private @@ -17,6 +13,14 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts: authorize ::Inbox end + def process_create + ActiveRecord::Base.transaction do + authenticate_twilio + build_inbox + setup_webhooks if @twilio_channel.sms? + end + end + def authenticate_twilio client = if permitted_params[:api_key_sid].present? Twilio::REST::Client.new(permitted_params[:api_key_sid], permitted_params[:auth_token], permitted_params[:account_sid]) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d2960a699..2f389049d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,3 +25,4 @@ class ApplicationController < ActionController::Base } end end +ApplicationController.include_mod_with('Concerns::ApplicationControllerConcern') diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index f3c85be7a..44592c201 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -163,10 +163,14 @@ class ConversationFinder params[:page] || 1 end - def conversations - @conversations = @conversations.includes( + def conversations_base_query + @conversations.includes( :taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox ) + end + + def conversations + @conversations = conversations_base_query sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc'] @conversations = @conversations.send(sort_by, sort_order) @@ -178,3 +182,4 @@ class ConversationFinder end end end +ConversationFinder.prepend_mod_with('ConversationFinder') diff --git a/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue b/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue new file mode 100644 index 000000000..381ca53b1 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue @@ -0,0 +1,103 @@ + + + diff --git a/app/javascript/dashboard/helper/directives/resize.js b/app/javascript/dashboard/helper/directives/resize.js new file mode 100644 index 000000000..35e5315b0 --- /dev/null +++ b/app/javascript/dashboard/helper/directives/resize.js @@ -0,0 +1,41 @@ +import { debounce } from '@chatwoot/utils'; + +const RESIZE_OBSERVER_DEBOUNCE_TIME = 100; + +function createResizeObserver(el, binding) { + const { value } = binding; + const observer = new ResizeObserver( + debounce(entries => { + const entry = entries[0]; + if (entry && value && typeof value === 'function') { + value(entry); + } + }, RESIZE_OBSERVER_DEBOUNCE_TIME) + ); + + el.cwResizeObserver = observer; + observer.observe(el); +} + +function destroyResizeObserver(el) { + if (el.cwResizeObserver) { + el.cwResizeObserver.unobserve(el); + el.cwResizeObserver.disconnect(); + delete el.cwResizeObserver; + } +} + +export default { + bind(el, binding) { + createResizeObserver(el, binding); + }, + update(el, binding) { + if (binding.oldValue !== binding.value) { + destroyResizeObserver(el); + createResizeObserver(el, binding); + } + }, + unbind(el) { + destroyResizeObserver(el); + }, +}; diff --git a/app/javascript/dashboard/helper/specs/directives/resize.spec.js b/app/javascript/dashboard/helper/specs/directives/resize.spec.js new file mode 100644 index 000000000..fa099f40a --- /dev/null +++ b/app/javascript/dashboard/helper/specs/directives/resize.spec.js @@ -0,0 +1,78 @@ +import resize from '../../directives/resize'; + +class ResizeObserverMock { + // eslint-disable-next-line class-methods-use-this + observe() {} + + // eslint-disable-next-line class-methods-use-this + unobserve() {} + + // eslint-disable-next-line class-methods-use-this + disconnect() {} +} + +describe('resize directive', () => { + let el; + let binding; + let observer; + + beforeEach(() => { + el = document.createElement('div'); + binding = { + value: jest.fn(), + }; + observer = { + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), + }; + window.ResizeObserver = ResizeObserverMock; + jest.spyOn(window, 'ResizeObserver').mockImplementation(() => observer); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should create ResizeObserver on bind', () => { + resize.bind(el, binding); + + expect(ResizeObserver).toHaveBeenCalled(); + expect(observer.observe).toHaveBeenCalledWith(el); + }); + + it('should call callback on observer callback', () => { + el = document.createElement('div'); + binding = { + value: jest.fn(), + }; + + resize.bind(el, binding); + + const entries = [{ contentRect: { width: 100, height: 100 } }]; + const callback = binding.value; + callback(entries[0]); + + expect(binding.value).toHaveBeenCalledWith(entries[0]); + }); + + it('should destroy and recreate observer on update', () => { + resize.bind(el, binding); + + resize.update(el, { ...binding, oldValue: 'old' }); + + expect(observer.unobserve).toHaveBeenCalledWith(el); + expect(observer.disconnect).toHaveBeenCalled(); + expect(ResizeObserver).toHaveBeenCalledTimes(2); + expect(observer.observe).toHaveBeenCalledTimes(2); + }); + + it('should destroy observer on unbind', () => { + resize.bind(el, binding); + + resize.unbind(el); + + expect(observer.unobserve).toHaveBeenCalledWith(el); + expect(observer.disconnect).toHaveBeenCalled(); + }); +}); diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 227c802d6..2bdf2af7a 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -64,7 +64,14 @@ "SNOOZED_UNTIL": "Snoozed until", "SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow", "SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week", - "SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply" + "SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply", + "SLA_STATUS": { + "FRT": "FRT {status}", + "NRT": "NRT {status}", + "RT": "RT {status}", + "BREACH": "breach", + "DUE": "due" + } }, "RESOLVE_DROPDOWN": { "MARK_PENDING": "Mark as pending", diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 9a4bde2c8..d9834c545 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -83,7 +83,10 @@ "CONVERSATION_CREATION": "Send email notifications when a new conversation is created", "CONVERSATION_MENTION": "Send email notifications when you are mentioned in a conversation", "ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send email notifications when a new message is created in an assigned conversation", - "PARTICIPATING_CONVERSATION_NEW_MESSAGE": "Send email notifications when a new message is created in a participating conversation" + "PARTICIPATING_CONVERSATION_NEW_MESSAGE": "Send email notifications when a new message is created in a participating conversation", + "SLA_MISSED_FIRST_RESPONSE": "Send email notifications when a conversation misses first response SLA", + "SLA_MISSED_NEXT_RESPONSE": "Send email notifications when a conversation misses next response SLA", + "SLA_MISSED_RESOLUTION": "Send email notifications when a conversation misses resolution SLA" }, "API": { "UPDATE_SUCCESS": "Your notification preferences are updated successfully", @@ -98,7 +101,10 @@ "ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send push notifications when a new message is created in an assigned conversation", "PARTICIPATING_CONVERSATION_NEW_MESSAGE": "Send push notifications when a new message is created in a participating conversation", "HAS_ENABLED_PUSH": "You have enabled push for this browser.", - "REQUEST_PUSH": "Enable push notifications" + "REQUEST_PUSH": "Enable push notifications", + "SLA_MISSED_FIRST_RESPONSE": "Send push notifications when a conversation misses first response SLA", + "SLA_MISSED_NEXT_RESPONSE": "Send push notifications when a conversation misses next response SLA", + "SLA_MISSED_RESOLUTION": "Send push notifications when a conversation misses resolution SLA" }, "PROFILE_IMAGE": { "LABEL": "Profile Image" diff --git a/app/javascript/dashboard/i18n/locale/en/sla.json b/app/javascript/dashboard/i18n/locale/en/sla.json index dcf8d2dca..d9670eb06 100644 --- a/app/javascript/dashboard/i18n/locale/en/sla.json +++ b/app/javascript/dashboard/i18n/locale/en/sla.json @@ -4,22 +4,9 @@ "ADD_ACTION": "Add SLA", "DESCRIPTION": "Service Level Agreements (SLAs) are contracts that define clear expectations between your team and customers. They establish standards for response and resolution times, creating a framework for accountability and ensures a consistent, high-quality experience.", "LEARN_MORE": "Learn more about SLA", - "HEADER_BTN_TXT": "Add SLA", "LOADING": "Fetching SLAs", - "SEARCH_404": "There are no items matching this query", - "SIDEBAR_TXT": "

SLA

Think of Service Level Agreements (SLAs) like friendly promises between a service provider and a customer.

These promises set clear expectations for things like how quickly the team will respond to issues, making sure you always get a reliable and top-notch experience!

", "LIST": { "404": "There are no SLAs available in this account.", - "TITLE": "Manage SLA", - "DESC": "SLAs: Friendly promises for great service!", - "TABLE_HEADER": [ - "Name", - "Description", - "FRT", - "NRT", - "RT", - "Business Hours" - ], "BUSINESS_HOURS_ON": "Business hours on", "BUSINESS_HOURS_OFF": "Business hours off", "RESPONSE_TYPES": { diff --git a/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue b/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue index 6dd3c306b..618805309 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/SettingsLayout.vue @@ -1,6 +1,23 @@ + diff --git a/app/javascript/dashboard/routes/dashboard/settings/components/BaseSettingsListItem.vue b/app/javascript/dashboard/routes/dashboard/settings/components/BaseSettingsListItem.vue index 005eece80..337c370cd 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/components/BaseSettingsListItem.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/components/BaseSettingsListItem.vue @@ -12,7 +12,7 @@ defineProps({