diff --git a/Gemfile b/Gemfile index 4c1ee452b..b350501d8 100644 --- a/Gemfile +++ b/Gemfile @@ -121,6 +121,8 @@ gem 'sentry-sidekiq', '>= 5.19.0', require: false gem 'sidekiq', '>= 7.3.1' # We want cron jobs gem 'sidekiq-cron', '>= 1.12.0' +# for sidekiq healthcheck +gem 'sidekiq_alive' ##-- Push notification service --## gem 'fcm' diff --git a/Gemfile.lock b/Gemfile.lock index 0b9daebbe..37ab9ea6d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -361,6 +361,7 @@ GEM grpc (1.72.0-x86_64-linux) google-protobuf (>= 3.25, < 5.0) googleapis-common-protos-types (~> 1.0) + gserver (0.0.1) haikunator (1.1.1) hairtrigger (1.0.0) activerecord (>= 6.0, < 8) @@ -479,7 +480,7 @@ GEM mime-types-data (3.2023.0218.1) mini_magick (4.12.0) mini_mime (1.1.5) - mini_portile2 (2.8.8) + mini_portile2 (2.8.9) minitest (5.25.5) mock_redis (0.36.0) ruby2_keywords @@ -510,14 +511,14 @@ GEM newrelic_rpm (9.6.0) base64 nio4r (2.7.3) - nokogiri (1.18.8) + nokogiri (1.18.9) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.18.8-arm64-darwin) + nokogiri (1.18.9-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.8-x86_64-darwin) + nokogiri (1.18.9-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.8-x86_64-linux-gnu) + nokogiri (1.18.9-x86_64-linux-gnu) racc (~> 1.4) oauth (1.1.0) oauth-tty (~> 1.0, >= 1.0.1) @@ -787,6 +788,9 @@ GEM fugit (~> 1.8) globalid (>= 1.0.1) sidekiq (>= 6) + sidekiq_alive (2.5.0) + gserver (~> 0.0.1) + sidekiq (>= 5, < 9) signet (0.17.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -825,7 +829,7 @@ GEM stripe (8.5.0) telephone_number (1.4.20) test-prof (1.2.1) - thor (1.3.1) + thor (1.4.0) tilt (2.3.0) time_diff (0.3.0) activesupport @@ -1012,6 +1016,7 @@ DEPENDENCIES shoulda-matchers sidekiq (>= 7.3.1) sidekiq-cron (>= 1.12.0) + sidekiq_alive simplecov (= 0.17.1) slack-ruby-client (~> 2.5.2) spring diff --git a/app/actions/contact_identify_action.rb b/app/actions/contact_identify_action.rb index a88d3535b..bcf5a93c3 100644 --- a/app/actions/contact_identify_action.rb +++ b/app/actions/contact_identify_action.rb @@ -6,6 +6,7 @@ # We don't want to update the name of the identified original contact. class ContactIdentifyAction + include UrlHelper pattr_initialize [:contact!, :params!, { retain_original_contact_name: false, discard_invalid_attrs: false }] def perform @@ -104,7 +105,14 @@ class ContactIdentifyAction # TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded @contact.discard_invalid_attrs if discard_invalid_attrs @contact.save! - Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? && !@contact.avatar.attached? + enqueue_avatar_job + end + + def enqueue_avatar_job + return unless params[:avatar_url].present? && !@contact.avatar.attached? + return unless url_valid?(params[:avatar_url]) + + Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) end def merge_contact(base_contact, merge_contact) diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 4fbe50902..039786905 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -122,7 +122,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def resolved_contacts return @resolved_contacts if @resolved_contacts - @resolved_contacts = Current.account.contacts.resolved_contacts + @resolved_contacts = Current.account.contacts.resolved_contacts(use_crm_v2: Current.account.feature_enabled?('crm_v2')) @resolved_contacts = @resolved_contacts.tagged_with(params[:labels], any: true) if params[:labels].present? @resolved_contacts diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index e7b3b197b..78b4b9e2f 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -69,6 +69,17 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') } end + def sync_templates + unless @inbox.channel.is_a?(Channel::Whatsapp) + return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' } + end + + Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel) + render status: :ok, json: { message: 'Template sync initiated successfully' } + rescue StandardError => e + render status: :internal_server_error, json: { error: e.message } + end + private def fetch_inbox diff --git a/app/controllers/microsoft_controller.rb b/app/controllers/microsoft_controller.rb index e6a12dafa..3071eac31 100644 --- a/app/controllers/microsoft_controller.rb +++ b/app/controllers/microsoft_controller.rb @@ -2,7 +2,7 @@ class MicrosoftController < ApplicationController after_action :set_version_header def identity_association - microsoft_indentity + microsoft_identity end private @@ -11,7 +11,7 @@ class MicrosoftController < ApplicationController response.headers['Content-Length'] = { associatedApplications: [{ applicationId: @identity_json }] }.to_json.length end - def microsoft_indentity + def microsoft_identity @identity_json = GlobalConfigService.load('AZURE_APP_ID', nil) end end diff --git a/app/javascript/dashboard/api/inboxes.js b/app/javascript/dashboard/api/inboxes.js index 8c09791c8..361b9472f 100644 --- a/app/javascript/dashboard/api/inboxes.js +++ b/app/javascript/dashboard/api/inboxes.js @@ -28,6 +28,10 @@ class Inboxes extends CacheEnabledApiClient { agent_bot: botId, }); } + + syncTemplates(inboxId) { + return axios.post(`${this.url}/${inboxId}/sync_templates`); + } } export default new Inboxes(); diff --git a/app/javascript/dashboard/api/specs/inboxes.spec.js b/app/javascript/dashboard/api/specs/inboxes.spec.js index 628ce0f34..64ba44aea 100644 --- a/app/javascript/dashboard/api/specs/inboxes.spec.js +++ b/app/javascript/dashboard/api/specs/inboxes.spec.js @@ -12,6 +12,7 @@ describe('#InboxesAPI', () => { expect(inboxesAPI).toHaveProperty('getCampaigns'); expect(inboxesAPI).toHaveProperty('getAgentBot'); expect(inboxesAPI).toHaveProperty('setAgentBot'); + expect(inboxesAPI).toHaveProperty('syncTemplates'); }); describe('API calls', () => { @@ -40,5 +41,12 @@ describe('#InboxesAPI', () => { inboxesAPI.deleteInboxAvatar(2); expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/inboxes/2/avatar'); }); + + it('#syncTemplates', () => { + inboxesAPI.syncTemplates(2); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/inboxes/2/sync_templates' + ); + }); }); }); diff --git a/app/javascript/dashboard/components-next/dropdown-menu/DropdownPrimitives.story.vue b/app/javascript/dashboard/components-next/dropdown-menu/DropdownPrimitives.story.vue index a174b1eea..397f45d93 100644 --- a/app/javascript/dashboard/components-next/dropdown-menu/DropdownPrimitives.story.vue +++ b/app/javascript/dashboard/components-next/dropdown-menu/DropdownPrimitives.story.vue @@ -6,7 +6,7 @@ import DropdownBody from './base/DropdownBody.vue'; import DropdownSection from './base/DropdownSection.vue'; import DropdownItem from './base/DropdownItem.vue'; import DropdownSeparator from './base/DropdownSeparator.vue'; -import WootSwitch from 'components/ui/Switch.vue'; +import ToggleSwitch from 'dashboard/components-next/switch/Switch.vue'; const currentUserAutoOffline = ref(false); @@ -61,7 +61,7 @@ const menuItems = ref([ {{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
- +
diff --git a/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenuStatus.vue b/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenuStatus.vue index fef196162..41e64dee1 100644 --- a/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenuStatus.vue +++ b/app/javascript/dashboard/components-next/sidebar/SidebarProfileMenuStatus.vue @@ -14,6 +14,7 @@ import { } from 'next/dropdown-menu/base'; import Icon from 'next/icon/Icon.vue'; import Button from 'next/button/Button.vue'; +import ToggleSwitch from 'dashboard/components-next/switch/Switch.vue'; const { t } = useI18n(); const store = useStore(); @@ -48,6 +49,16 @@ const activeStatus = computed(() => { return availabilityStatuses.value.find(status => status.active); }); +const autoOfflineToggle = computed({ + get: () => currentUserAutoOffline.value, + set: autoOffline => { + store.dispatch('updateAutoOffline', { + accountId: currentAccountId.value, + autoOffline, + }); + }, +}); + function changeAvailabilityStatus(availability) { if (isImpersonating.value) { useAlert(t('PROFILE_SETTINGS.FORM.AVAILABILITY.IMPERSONATING_ERROR')); @@ -62,13 +73,6 @@ function changeAvailabilityStatus(availability) { useAlert(t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')); } } - -function updateAutoOffline(autoOffline) { - store.dispatch('updateAutoOffline', { - accountId: currentAccountId.value, - autoOffline, - }); -}