diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb index 18538e842..af96441f8 100644 --- a/app/controllers/api/v1/accounts/portals_controller.rb +++ b/app/controllers/api/v1/accounts/portals_controller.rb @@ -26,9 +26,8 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController @portal.update!(portal_params.merge(live_chat_widget_params)) if params[:portal].present? # @portal.custom_domain = parsed_custom_domain process_attached_logo if params[:blob_id].present? - rescue StandardError => e - Rails.logger.error e - render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity + rescue ActiveRecord::RecordInvalid => e + render_record_invalid(e) end end diff --git a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb index e7a1f3fa6..3e7d876c3 100644 --- a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb +++ b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb @@ -1,8 +1,10 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController before_action :validate_feature_enabled! + before_action :fetch_and_validate_inbox, if: -> { params[:inbox_id].present? } # POST /api/v1/accounts/:account_id/whatsapp/authorization - # Handles the embedded signup callback data from the Facebook SDK + # Handles both initial authorization and reauthorization + # If inbox_id is present in params, it performs reauthorization def create validate_embedded_signup_params! channel = process_embedded_signup @@ -16,21 +18,42 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts: def process_embedded_signup service = Whatsapp::EmbeddedSignupService.new( account: Current.account, - code: params[:code], - business_id: params[:business_id], - waba_id: params[:waba_id], - phone_number_id: params[:phone_number_id] + params: params.permit(:code, :business_id, :waba_id, :phone_number_id).to_h.symbolize_keys, + inbox_id: params[:inbox_id] ) service.perform end - def render_success_response(inbox) + def fetch_and_validate_inbox + @inbox = Current.account.inboxes.find(params[:inbox_id]) + validate_reauthorization_required + end + + def validate_reauthorization_required + return if @inbox.channel.reauthorization_required? || can_upgrade_to_embedded_signup? + render json: { + success: false, + message: I18n.t('inbox.reauthorization.not_required') + }, status: :unprocessable_entity + end + + def can_upgrade_to_embedded_signup? + channel = @inbox.channel + return false unless channel.provider == 'whatsapp_cloud' + + true + end + + def render_success_response(inbox) + response = { success: true, id: inbox.id, name: inbox.name, channel_type: 'whatsapp' } + response[:message] = I18n.t('inbox.reauthorization.success') if params[:inbox_id].present? + render json: response end def render_error_response(error) diff --git a/app/controllers/platform/api/v1/accounts_controller.rb b/app/controllers/platform/api/v1/accounts_controller.rb index e11cf9d4a..4521930a6 100644 --- a/app/controllers/platform/api/v1/accounts_controller.rb +++ b/app/controllers/platform/api/v1/accounts_controller.rb @@ -1,4 +1,11 @@ class Platform::Api::V1::AccountsController < PlatformController + def index + @resources = @platform_app.platform_app_permissibles + .where(permissible_type: 'Account') + .includes(:permissible) + .map(&:permissible) + end + def show; end def create diff --git a/app/controllers/slack_uploads_controller.rb b/app/controllers/slack_uploads_controller.rb index 127e77649..6f157c7d5 100644 --- a/app/controllers/slack_uploads_controller.rb +++ b/app/controllers/slack_uploads_controller.rb @@ -17,7 +17,12 @@ class SlackUploadsController < ApplicationController end def blob_url - url_for(@blob.representation(resize_to_fill: [250, nil])) + # Only generate representations for images + if @blob.content_type.start_with?('image/') + url_for(@blob.representation(resize_to_fill: [250, nil])) + else + url_for(@blob) + end end def avatar_url diff --git a/app/controllers/twilio/callback_controller.rb b/app/controllers/twilio/callback_controller.rb index 455828228..d607ba151 100644 --- a/app/controllers/twilio/callback_controller.rb +++ b/app/controllers/twilio/callback_controller.rb @@ -30,7 +30,8 @@ class Twilio::CallbackController < ApplicationController :NumMedia, :Latitude, :Longitude, - :MessageType + :MessageType, + :ProfileName ) end end diff --git a/app/finders/notification_finder.rb b/app/finders/notification_finder.rb index ccfe470a0..e1958827a 100644 --- a/app/finders/notification_finder.rb +++ b/app/finders/notification_finder.rb @@ -15,7 +15,13 @@ class NotificationFinder end def unread_count - @notifications.where(read_at: nil).count + if type_included?('read') + # If we're including read notifications, filter to unread + @notifications.where(read_at: nil).count + else + # Already filtered to unread notifications, just count + @notifications.count + end end def count @@ -27,7 +33,7 @@ class NotificationFinder def set_up find_all_notifications filter_snoozed_notifications - fitler_read_notifications + filter_read_notifications end def find_all_notifications @@ -38,7 +44,7 @@ class NotificationFinder @notifications = @notifications.where(snoozed_until: nil) unless type_included?('snoozed') end - def fitler_read_notifications + def filter_read_notifications @notifications = @notifications.where(read_at: nil) unless type_included?('read') end diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 0fbb20ea9..8da7e7476 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -137,7 +137,6 @@ export default { v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem" id="app" class="flex flex-col w-full h-screen min-h-0" - :class="{ 'app-rtl--wrapper': isRTL }" :dir="isRTL ? 'rtl' : 'ltr'" > diff --git a/app/javascript/dashboard/api/channel/whatsappChannel.js b/app/javascript/dashboard/api/channel/whatsappChannel.js index e1003b123..8f51f4878 100644 --- a/app/javascript/dashboard/api/channel/whatsappChannel.js +++ b/app/javascript/dashboard/api/channel/whatsappChannel.js @@ -9,6 +9,13 @@ class WhatsappChannel extends ApiClient { createEmbeddedSignup(params) { return axios.post(`${this.baseUrl()}/whatsapp/authorization`, params); } + + reauthorizeWhatsApp({ inboxId, ...params }) { + return axios.post(`${this.baseUrl()}/whatsapp/authorization`, { + ...params, + inbox_id: inboxId, + }); + } } export default new WhatsappChannel(); diff --git a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue index 304e55347..05507fc89 100644 --- a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue +++ b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue @@ -76,8 +76,8 @@ const campaignStatus = computed(() => { const inboxName = computed(() => props.inbox?.name || ''); const inboxIcon = computed(() => { - const { phone_number: phoneNumber, channel_type: type } = props.inbox; - return getInboxIconByType(type, phoneNumber); + const { medium, channel_type: type } = props.inbox; + return getInboxIconByType(type, medium); }); diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue index 12a789fee..f4301375d 100644 --- a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue @@ -38,11 +38,13 @@ const handleClose = () => emit('close'); diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue index 0932a79c7..0e893b767 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue @@ -98,6 +98,7 @@ const onClickViewDetails = () => emit('showContact', props.id); :src="thumbnail" :size="48" :status="availabilityStatus" + hide-offline-status rounded-full />
diff --git a/app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCard.vue b/app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCard.vue index 36f611775..f9a2507a1 100644 --- a/app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCard.vue +++ b/app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCard.vue @@ -48,8 +48,8 @@ const inbox = computed(() => props.stateInbox); const inboxName = computed(() => inbox.value?.name); const inboxIcon = computed(() => { - const { phoneNumber, channelType } = inbox.value; - return getInboxIconByType(channelType, phoneNumber); + const { channelType, medium } = inbox.value; + return getInboxIconByType(channelType, medium); }); const lastActivityAt = computed(() => { diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue index 7995270b5..862a2cd67 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue @@ -7,8 +7,8 @@ import { useStore, useStoreGetters } from 'dashboard/composables/store'; import { uploadFile } from 'dashboard/helper/uploadHelper'; import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; import { useVuelidate } from '@vuelidate/core'; -import { required, minLength, helpers } from '@vuelidate/validators'; -import { shouldBeUrl, isValidSlug } from 'shared/helpers/Validators'; +import { required, minLength, helpers, url } from '@vuelidate/validators'; +import { isValidSlug } from 'shared/helpers/Validators'; import Button from 'dashboard/components-next/button/Button.vue'; import Input from 'dashboard/components-next/input/Input.vue'; @@ -71,7 +71,7 @@ const rules = { isValidSlug ), }, - homePageLink: { shouldBeUrl }, + homePageLink: { url }, }; const v$ = useVuelidate(rules, state); diff --git a/app/javascript/dashboard/components-next/Inbox/InboxCard.vue b/app/javascript/dashboard/components-next/Inbox/InboxCard.vue index 90cc1ff53..958f25048 100644 --- a/app/javascript/dashboard/components-next/Inbox/InboxCard.vue +++ b/app/javascript/dashboard/components-next/Inbox/InboxCard.vue @@ -49,8 +49,8 @@ const isUnread = computed(() => !props.inboxItem?.readAt); const inbox = computed(() => props.stateInbox); const inboxIcon = computed(() => { - const { phoneNumber, channelType } = inbox.value; - return getInboxIconByType(channelType, phoneNumber); + const { channelType, medium } = inbox.value; + return getInboxIconByType(channelType, medium); }); const hasSlaThreshold = computed(() => { diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue index 90cb67c4f..a686a8d16 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue @@ -170,7 +170,7 @@ useKeyboardEvents(keyboardEvents); />
diff --git a/app/javascript/dashboard/components-next/NewConversation/components/AttachmentPreviews.vue b/app/javascript/dashboard/components-next/NewConversation/components/AttachmentPreviews.vue index fe57d19c4..a546fe0df 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/AttachmentPreviews.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/AttachmentPreviews.vue @@ -57,7 +57,7 @@ const removeAttachment = id => { variant="ghost" icon="i-lucide-trash" color="slate" - class="absolute top-1 right-1 !w-5 !h-5 transition-opacity duration-150 ease-in-out opacity-0 group-hover/image:opacity-100" + class="absolute top-1 ltr:right-1 rtl:left-1 !w-5 !h-5 transition-opacity duration-150 ease-in-out opacity-0 group-hover/image:opacity-100" @click="removeAttachment(attachment.resource.id)" /> diff --git a/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue b/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue index 7794d5bdd..3cafacb21 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue @@ -83,7 +83,7 @@ const targetInboxLabel = computed(() => { diff --git a/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue b/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue index 3980c6b30..124612ad3 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue @@ -2,6 +2,7 @@ import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; +import Icon from 'dashboard/components-next/icon/Icon.vue'; import Button from 'dashboard/components-next/button/Button.vue'; import WhatsappTemplateParser from './WhatsappTemplateParser.vue'; @@ -84,10 +85,13 @@ const handleSendMessage = template => { />
- + { 'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.SEARCH_PLACEHOLDER' ) " - class="w-full h-8 py-2 pl-10 pr-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12" + class="w-full h-8 py-2 ltr:pl-10 rtl:pr-10 ltr:pr-2 rtl:pl-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12" />
{ diff --git a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue index 30fe85b0a..f349720b0 100644 --- a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue +++ b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue @@ -57,9 +57,10 @@ const menuItems = computed(() => [ }, ]); -const icon = computed(() => - getInboxIconByType(props.inbox.channel_type, '', 'outline') -); +const icon = computed(() => { + const { medium, channel_type: type } = props.inbox; + return getInboxIconByType(type, medium, 'outline'); +}); const handleAction = ({ action, value }) => { toggleDropdown(false); diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue index 8c26de2f5..ecf13c286 100644 --- a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue +++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue @@ -35,6 +35,7 @@ const initialState = { productName: '', featureFaq: false, featureMemory: false, + featureCitation: false, }; const state = reactive({ ...initialState }); @@ -70,6 +71,7 @@ const prepareAssistantDetails = () => ({ product_name: state.productName, feature_faq: state.featureFaq, feature_memory: state.featureMemory, + feature_citation: state.featureCitation, }, }); @@ -93,6 +95,7 @@ const updateStateFromAssistant = assistant => { productName: config.product_name, featureFaq: config.feature_faq || false, featureMemory: config.feature_memory || false, + featureCitation: config.feature_citation || false, }); }; @@ -151,6 +154,13 @@ watch( {{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }} + +
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue index 3134d7062..cca996a44 100644 --- a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue +++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue @@ -41,6 +41,7 @@ const initialState = { features: { conversationFaqs: false, memories: false, + citations: false, }, temperature: 1, }; @@ -87,6 +88,7 @@ const updateStateFromAssistant = assistant => { state.features = { conversationFaqs: config.feature_faq || false, memories: config.feature_memory || false, + citations: config.feature_citation || false, }; state.temperature = config.temperature || 1; }; @@ -152,6 +154,7 @@ const handleFeaturesUpdate = () => { ...props.assistant.config, feature_faq: state.features.conversationFaqs, feature_memory: state.features.memories, + feature_citation: state.features.citations, }, }; @@ -300,20 +303,19 @@ watch( {{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_CONVERSATION_FAQS') }} +
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue index 236791793..42c075ea1 100644 --- a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue +++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue @@ -26,6 +26,7 @@ const initialState = { features: { conversationFaqs: false, memories: false, + citations: false, }, }; @@ -57,6 +58,7 @@ const updateStateFromAssistant = assistant => { state.features = { conversationFaqs: config.feature_faq || false, memories: config.feature_memory || false, + citations: config.feature_citation || false, }; }; @@ -76,6 +78,7 @@ const handleBasicInfoUpdate = async () => { product_name: state.productName, feature_faq: state.features.conversationFaqs, feature_memory: state.features.memories, + feature_citation: state.features.citations, }, }; @@ -123,21 +126,17 @@ watch(
+
diff --git a/app/javascript/dashboard/components-next/icon/ChannelIcon.vue b/app/javascript/dashboard/components-next/icon/ChannelIcon.vue index 11117cc18..68102dbd3 100644 --- a/app/javascript/dashboard/components-next/icon/ChannelIcon.vue +++ b/app/javascript/dashboard/components-next/icon/ChannelIcon.vue @@ -1,4 +1,5 @@