diff --git a/.env.example b/.env.example index de671599c..3ff349ffc 100644 --- a/.env.example +++ b/.env.example @@ -256,6 +256,8 @@ AZURE_APP_SECRET= ## Change these values to fine tune performance # control the concurrency setting of sidekiq # SIDEKIQ_CONCURRENCY=10 +# Enable verbose logging each time a job is dequeued in Sidekiq +# ENABLE_SIDEKIQ_DEQUEUE_LOGGER=false # AI powered features diff --git a/.github/workflows/run_mfa_spec.yml b/.github/workflows/run_mfa_spec.yml index 61b406f8a..69d019cc9 100644 --- a/.github/workflows/run_mfa_spec.yml +++ b/.github/workflows/run_mfa_spec.yml @@ -70,6 +70,7 @@ jobs: spec/services/mfa/authentication_service_spec.rb \ spec/requests/api/v1/profile/mfa_controller_spec.rb \ spec/controllers/devise_overrides/sessions_controller_spec.rb \ + spec/models/application_record_external_credentials_encryption_spec.rb \ --profile=10 \ --format documentation env: diff --git a/.rubocop.yml b/.rubocop.yml index e30a71ee9..ea688792b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -23,7 +23,7 @@ Metrics/MethodLength: - 'enterprise/lib/captain/agent.rb' RSpec/ExampleLength: - Max: 25 + Max: 50 Style/Documentation: Enabled: false @@ -336,4 +336,4 @@ FactoryBot/RedundantFactoryOption: Enabled: false FactoryBot/FactoryAssociationWithStrategy: - Enabled: false \ No newline at end of file + Enabled: false diff --git a/Gemfile b/Gemfile index 18442e3b0..abbd3332f 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,7 @@ gem 'telephone_number' gem 'time_diff' gem 'tzinfo-data' gem 'valid_email2' +gem 'email-provider-info' # compress javascript config.assets.js_compressor gem 'uglifier' ##-- used for single column multiple binary flags in notification settings/feature flagging --## diff --git a/Gemfile.lock b/Gemfile.lock index 105bf8c13..99e75b33c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -270,6 +270,7 @@ GEM concurrent-ruby (~> 1.0) http (>= 3.0) ruby2_keywords + email-provider-info (0.0.1) email_reply_trimmer (0.1.13) erubi (1.13.0) et-orbi (1.2.11) @@ -594,7 +595,7 @@ GEM oj (3.16.10) bigdecimal (>= 3.0) ostruct (>= 0.2) - omniauth (2.1.3) + omniauth (2.1.4) hashie (>= 3.4.6) logger rack (>= 2.2.3) @@ -644,7 +645,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (3.2.0) + rack (3.2.3) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-contrib (2.5.0) @@ -653,7 +654,7 @@ GEM rack (>= 2.0.0) rack-mini-profiler (3.2.0) rack (>= 1.2.0) - rack-protection (4.1.1) + rack-protection (4.2.1) base64 (>= 0.1.0) logger (>= 1.6.0) rack (>= 3.0.0, < 4) @@ -935,7 +936,7 @@ GEM unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) uniform_notifier (1.17.0) - uri (1.0.3) + uri (1.0.4) uri_template (0.7.0) valid_email2 (5.2.6) activemodel (>= 3.2) @@ -1016,6 +1017,7 @@ DEPENDENCIES dotenv-rails (>= 3.0.0) down elastic-apm + email-provider-info email_reply_trimmer facebook-messenger factory_bot_rails (>= 6.4.3) diff --git a/app.json b/app.json index 08e725c8e..91fb0fbd5 100644 --- a/app.json +++ b/app.json @@ -36,6 +36,10 @@ "REDIS_OPENSSL_VERIFY_MODE":{ "description": "OpenSSL verification mode for Redis connections. ref https://help.heroku.com/HC0F8CUS/redis-connection-issues", "value": "none" + }, + "NODE_OPTIONS": { + "description": "Increase V8 heap for Vite build to avoid OOM", + "value": "--max-old-space-size=4096" } }, "formation": { diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index 86bcee54e..857d901e5 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -23,7 +23,7 @@ class Messages::MessageBuilder process_emails # When the message has no quoted content, it will just be rendered as a regular message # The frontend is equipped to handle this case - process_email_content if @account.feature_enabled?(:quoted_email_reply) + process_email_content @message.save! @message end @@ -178,7 +178,13 @@ class Messages::MessageBuilder email_attributes = ensure_indifferent_access(@message.content_attributes[:email] || {}) normalized_content = normalize_email_body(@message.content) - email_attributes[:html_content] = build_html_content(normalized_content) + # Use custom HTML content if provided, otherwise generate from message content + email_attributes[:html_content] = if custom_email_content_provided? + build_custom_html_content + else + build_html_content(normalized_content) + end + email_attributes[:text_content] = build_text_content(normalized_content) email_attributes end @@ -213,4 +219,17 @@ class Messages::MessageBuilder ChatwootMarkdownRenderer.new(content).render_message.to_s end + + def custom_email_content_provided? + @params[:email_html_content].present? + end + + def build_custom_html_content + html_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :html_content) || {}) + + html_content[:full] = @params[:email_html_content] + html_content[:reply] = @params[:email_html_content] + + html_content + end end diff --git a/app/builders/v2/reports/base_summary_builder.rb b/app/builders/v2/reports/base_summary_builder.rb index 4de65926d..d4a9e7c0b 100644 --- a/app/builders/v2/reports/base_summary_builder.rb +++ b/app/builders/v2/reports/base_summary_builder.rb @@ -10,10 +10,28 @@ class V2::Reports::BaseSummaryBuilder def load_data @conversations_count = fetch_conversations_count - @resolved_count = fetch_resolved_count - @avg_resolution_time = fetch_average_time('conversation_resolved') - @avg_first_response_time = fetch_average_time('first_response') - @avg_reply_time = fetch_average_time('reply_time') + load_reporting_events_data + end + + def load_reporting_events_data + # Extract the column name for indexing (e.g., 'conversations.team_id' -> 'team_id') + index_key = group_by_key.to_s.split('.').last + + results = reporting_events + .select( + "#{group_by_key} as #{index_key}", + "COUNT(CASE WHEN name = 'conversation_resolved' THEN 1 END) as resolved_count", + "AVG(CASE WHEN name = 'conversation_resolved' THEN #{average_value_key} END) as avg_resolution_time", + "AVG(CASE WHEN name = 'first_response' THEN #{average_value_key} END) as avg_first_response_time", + "AVG(CASE WHEN name = 'reply_time' THEN #{average_value_key} END) as avg_reply_time" + ) + .group(group_by_key) + .index_by { |record| record.public_send(index_key) } + + @resolved_count = results.transform_values(&:resolved_count) + @avg_resolution_time = results.transform_values(&:avg_resolution_time) + @avg_first_response_time = results.transform_values(&:avg_first_response_time) + @avg_reply_time = results.transform_values(&:avg_reply_time) end def reporting_events @@ -24,14 +42,6 @@ class V2::Reports::BaseSummaryBuilder # Override this method end - def fetch_average_time(event_name) - get_grouped_average(reporting_events.where(name: event_name)) - end - - def fetch_resolved_count - reporting_events.where(name: 'conversation_resolved').group(group_by_key).count - end - def group_by_key # Override this method end @@ -40,10 +50,6 @@ class V2::Reports::BaseSummaryBuilder # Override this method end - def get_grouped_average(events) - events.group(group_by_key).average(average_value_key) - end - def average_value_key ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value end diff --git a/app/builders/v2/reports/inbox_summary_builder.rb b/app/builders/v2/reports/inbox_summary_builder.rb index e27385856..935afeb82 100644 --- a/app/builders/v2/reports/inbox_summary_builder.rb +++ b/app/builders/v2/reports/inbox_summary_builder.rb @@ -13,10 +13,7 @@ class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder def load_data @conversations_count = fetch_conversations_count - @resolved_count = fetch_resolved_count - @avg_resolution_time = fetch_average_time('conversation_resolved') - @avg_first_response_time = fetch_average_time('first_response') - @avg_reply_time = fetch_average_time('reply_time') + load_reporting_events_data end def fetch_conversations_count diff --git a/app/controllers/api/v1/accounts/bulk_actions_controller.rb b/app/controllers/api/v1/accounts/bulk_actions_controller.rb index 34db47861..222c66714 100644 --- a/app/controllers/api/v1/accounts/bulk_actions_controller.rb +++ b/app/controllers/api/v1/accounts/bulk_actions_controller.rb @@ -1,13 +1,12 @@ class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseController - before_action :type_matches? - def create - if type_matches? - ::BulkActionsJob.perform_later( - account: @current_account, - user: current_user, - params: permitted_params - ) + case normalized_type + when 'Conversation' + enqueue_conversation_job + head :ok + when 'Contact' + check_authorization_for_contact_action + enqueue_contact_job head :ok else render json: { success: false }, status: :unprocessable_entity @@ -16,11 +15,54 @@ class Api::V1::Accounts::BulkActionsController < Api::V1::Accounts::BaseControll private - def type_matches? - ['Conversation'].include?(params[:type]) + def normalized_type + params[:type].to_s.camelize end - def permitted_params - params.permit(:type, :snoozed_until, ids: [], fields: [:status, :assignee_id, :team_id], labels: [add: [], remove: []]) + def enqueue_conversation_job + ::BulkActionsJob.perform_later( + account: @current_account, + user: current_user, + params: conversation_params + ) + end + + def enqueue_contact_job + Contacts::BulkActionJob.perform_later( + @current_account.id, + current_user.id, + contact_params + ) + end + + def delete_contact_action? + params[:action_name] == 'delete' + end + + def check_authorization_for_contact_action + authorize(Contact, :destroy?) if delete_contact_action? + end + + def conversation_params + # TODO: Align conversation payloads with the `{ action_name, action_attributes }` + # and then remove this method in favor of a common params method. + base = params.permit( + :snoozed_until, + fields: [:status, :assignee_id, :team_id] + ) + append_common_bulk_attributes(base) + end + + def contact_params + # TODO: remove this method in favor of a common params method. + # once legacy conversation payloads are migrated. + append_common_bulk_attributes({}) + end + + def append_common_bulk_attributes(base_params) + # NOTE: Conversation payloads historically diverged per action. Going forward we + # want all objects to share a common contract: `{ action_name, action_attributes }` + common = params.permit(:type, :action_name, ids: [], labels: [add: [], remove: []]) + base_params.merge(common) end end diff --git a/app/controllers/api/v1/accounts/conversations/base_controller.rb b/app/controllers/api/v1/accounts/conversations/base_controller.rb index 500c7772f..223530e27 100644 --- a/app/controllers/api/v1/accounts/conversations/base_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/base_controller.rb @@ -5,6 +5,6 @@ class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::Base def conversation @conversation ||= Current.account.conversations.find_by!(display_id: params[:conversation_id]) - authorize @conversation.inbox, :show? + authorize @conversation, :show? end end diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index e27869d82..4301eaa4a 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -160,7 +160,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro def conversation @conversation ||= Current.account.conversations.find_by!(display_id: params[:id]) - authorize @conversation.inbox, :show? + authorize @conversation, :show? end def inbox diff --git a/app/controllers/api/v1/accounts/integrations/dyte_controller.rb b/app/controllers/api/v1/accounts/integrations/dyte_controller.rb index c5f795d34..845caab5e 100644 --- a/app/controllers/api/v1/accounts/integrations/dyte_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/dyte_controller.rb @@ -22,7 +22,7 @@ class Api::V1::Accounts::Integrations::DyteController < Api::V1::Accounts::BaseC private def authorize_request - authorize @conversation.inbox, :show? + authorize @conversation, :show? end def render_response(response) diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index 9b0f9021f..338b290da 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -14,6 +14,7 @@ module AccessTokenAuthHelper ensure_access_token render_unauthorized('Invalid Access Token') && return if @access_token.blank? + # NOTE: This ensures that current_user is set and available for the rest of the controller actions @resource = @access_token.owner Current.user = @resource if allowed_current_user_type?(@resource) end diff --git a/app/controllers/devise_overrides/omniauth_callbacks_controller.rb b/app/controllers/devise_overrides/omniauth_callbacks_controller.rb index fd3dba87c..900125670 100644 --- a/app/controllers/devise_overrides/omniauth_callbacks_controller.rb +++ b/app/controllers/devise_overrides/omniauth_callbacks_controller.rb @@ -19,6 +19,19 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa redirect_to login_page_url(email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token) end + def sign_in_user_on_mobile + @resource.skip_confirmation! if confirmable_enabled? + + # once the resource is found and verified + # we can just send them to the login page again with the SSO params + # that will log them in + encoded_email = ERB::Util.url_encode(@resource.email) + params = { email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token }.to_query + + mobile_deep_link_base = GlobalConfigService.load('MOBILE_DEEP_LINK_BASE', 'chatwootapp') + redirect_to "#{mobile_deep_link_base}://auth/saml?#{params}", allow_other_host: true + end + def sign_up_user return redirect_to login_page_url(error: 'no-account-found') unless account_signup_allowed? return redirect_to login_page_url(error: 'business-account-only') unless validate_signup_email_is_business_domain? diff --git a/app/javascript/dashboard/api/captain/customTools.js b/app/javascript/dashboard/api/captain/customTools.js new file mode 100644 index 000000000..d0818d941 --- /dev/null +++ b/app/javascript/dashboard/api/captain/customTools.js @@ -0,0 +1,36 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class CaptainCustomTools extends ApiClient { + constructor() { + super('captain/custom_tools', { accountScoped: true }); + } + + get({ page = 1, searchKey } = {}) { + return axios.get(this.url, { + params: { page, searchKey }, + }); + } + + show(id) { + return axios.get(`${this.url}/${id}`); + } + + create(data = {}) { + return axios.post(this.url, { + custom_tool: data, + }); + } + + update(id, data = {}) { + return axios.put(`${this.url}/${id}`, { + custom_tool: data, + }); + } + + delete(id) { + return axios.delete(`${this.url}/${id}`); + } +} + +export default new CaptainCustomTools(); diff --git a/app/javascript/dashboard/api/changelog.js b/app/javascript/dashboard/api/changelog.js new file mode 100644 index 000000000..8cf0cdea1 --- /dev/null +++ b/app/javascript/dashboard/api/changelog.js @@ -0,0 +1,16 @@ +import axios from 'axios'; +import ApiClient from './ApiClient'; +import { CHANGELOG_API_URL } from 'shared/constants/links'; + +class ChangelogApi extends ApiClient { + constructor() { + super('changelog', { apiVersion: 'v1' }); + } + + // eslint-disable-next-line class-methods-use-this + fetchFromHub() { + return axios.get(CHANGELOG_API_URL); + } +} + +export default new ChangelogApi(); diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue index 47b779b61..041f06410 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue @@ -5,6 +5,7 @@ import { useToggle } from '@vueuse/core'; import Button from 'dashboard/components-next/button/Button.vue'; import ConfirmContactDeleteDialog from 'dashboard/components-next/Contacts/ContactsForm/ConfirmContactDeleteDialog.vue'; +import Policy from 'dashboard/components/policy.vue'; defineProps({ selectedContact: { @@ -24,42 +25,44 @@ const openConfirmDeleteContactDialog = () => { diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue index 0e893b767..12bed151d 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue @@ -8,6 +8,7 @@ import Button from 'dashboard/components-next/button/Button.vue'; import Avatar from 'dashboard/components-next/avatar/Avatar.vue'; import Flag from 'dashboard/components-next/flag/Flag.vue'; import ContactDeleteSection from 'dashboard/components-next/Contacts/ContactsCard/ContactDeleteSection.vue'; +import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue'; import countries from 'shared/constants/countries'; const props = defineProps({ @@ -20,9 +21,17 @@ const props = defineProps({ availabilityStatus: { type: String, default: null }, isExpanded: { type: Boolean, default: false }, isUpdating: { type: Boolean, default: false }, + selectable: { type: Boolean, default: false }, + isSelected: { type: Boolean, default: false }, }); -const emit = defineEmits(['toggle', 'updateContact', 'showContact']); +const emit = defineEmits([ + 'toggle', + 'updateContact', + 'showContact', + 'select', + 'avatarHover', +]); const { t } = useI18n(); @@ -88,111 +97,148 @@ const onClickExpand = () => { }; const onClickViewDetails = () => emit('showContact', props.id); + +const toggleSelect = checked => { + emit('select', checked); +}; + +const handleAvatarHover = isHovered => { + emit('avatarHover', isHovered); +}; diff --git a/app/javascript/dashboard/components-next/Contacts/Pages/ContactDetails.vue b/app/javascript/dashboard/components-next/Contacts/Pages/ContactDetails.vue index aec21a976..b7014c42f 100644 --- a/app/javascript/dashboard/components-next/Contacts/Pages/ContactDetails.vue +++ b/app/javascript/dashboard/components-next/Contacts/Pages/ContactDetails.vue @@ -10,6 +10,7 @@ import Button from 'dashboard/components-next/button/Button.vue'; import ContactLabels from 'dashboard/components-next/Contacts/ContactLabels/ContactLabels.vue'; import ContactsForm from 'dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue'; import ConfirmContactDeleteDialog from 'dashboard/components-next/Contacts/ContactsForm/ConfirmContactDeleteDialog.vue'; +import Policy from 'dashboard/components/policy.vue'; const props = defineProps({ selectedContact: { @@ -174,27 +175,29 @@ const handleAvatarDelete = async () => { @click="updateContact" /> -
-
-
- {{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT') }} -
- - {{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT_DESCRIPTION') }} - + +
+
+
+ {{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT') }} +
+ + {{ t('CONTACTS_LAYOUT.DETAILS.DELETE_CONTACT_DESCRIPTION') }} + +
+
-
- +
diff --git a/app/javascript/dashboard/components-next/Contacts/Pages/ContactsList.vue b/app/javascript/dashboard/components-next/Contacts/Pages/ContactsList.vue index 7acc2ff55..72bc2c7dd 100644 --- a/app/javascript/dashboard/components-next/Contacts/Pages/ContactsList.vue +++ b/app/javascript/dashboard/components-next/Contacts/Pages/ContactsList.vue @@ -10,7 +10,15 @@ import { } from 'shared/helpers/CustomErrors'; import ContactsCard from 'dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue'; -defineProps({ contacts: { type: Array, required: true } }); +const props = defineProps({ + contacts: { type: Array, required: true }, + selectedContactIds: { + type: Array, + default: () => [], + }, +}); + +const emit = defineEmits(['toggleContact']); const { t } = useI18n(); const store = useStore(); @@ -20,6 +28,9 @@ const route = useRoute(); const uiFlags = useMapGetter('contacts/getUIFlags'); const isUpdating = computed(() => uiFlags.value.isUpdating); const expandedCardId = ref(null); +const hoveredAvatarId = ref(null); + +const selectedIdsSet = computed(() => new Set(props.selectedContactIds || [])); const updateContact = async updatedData => { try { @@ -58,25 +69,43 @@ const onClickViewDetails = async id => { const toggleExpanded = id => { expandedCardId.value = expandedCardId.value === id ? null : id; }; + +const isSelected = id => selectedIdsSet.value.has(id); + +const shouldShowSelection = id => { + return hoveredAvatarId.value === id || isSelected(id); +}; + +const handleSelect = (id, value) => { + emit('toggleContact', { id, value }); +}; + +const handleAvatarHover = (id, isHovered) => { + hoveredAvatarId.value = isHovered ? id : null; +}; diff --git a/app/javascript/dashboard/components-next/EmptyStateLayout.vue b/app/javascript/dashboard/components-next/EmptyStateLayout.vue index f2744dde5..39eda2d85 100644 --- a/app/javascript/dashboard/components-next/EmptyStateLayout.vue +++ b/app/javascript/dashboard/components-next/EmptyStateLayout.vue @@ -14,6 +14,10 @@ defineProps({ type: Array, default: () => [], }, + showBackdrop: { + type: Boolean, + default: true, + }, }); @@ -25,14 +29,24 @@ defineProps({ class="relative w-full max-w-[60rem] mx-auto overflow-hidden h-full max-h-[28rem]" >
-
+

{{ subtitle }} diff --git a/app/javascript/dashboard/components-next/captain/PageLayout.vue b/app/javascript/dashboard/components-next/captain/PageLayout.vue index 495db1838..c394f1b6b 100644 --- a/app/javascript/dashboard/components-next/captain/PageLayout.vue +++ b/app/javascript/dashboard/components-next/captain/PageLayout.vue @@ -114,6 +114,7 @@ const handlePageChange = event => {

+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/BulkSelectBar.vue b/app/javascript/dashboard/components-next/captain/assistant/BulkSelectBar.vue index 94cab9a33..0bf2007ff 100644 --- a/app/javascript/dashboard/components-next/captain/assistant/BulkSelectBar.vue +++ b/app/javascript/dashboard/components-next/captain/assistant/BulkSelectBar.vue @@ -64,20 +64,23 @@ const bulkCheckboxState = computed({ class="flex items-center gap-3 py-1 ltr:pl-3 rtl:pr-3 ltr:pr-4 rtl:pl-4 rounded-lg bg-n-solid-2 outline outline-1 outline-n-container shadow" >
-
+
- + {{ selectAllLabel }}
- + {{ selectedCountLabel }} +
+
-
+ +
+
- - + + {{ assistant?.name || '' }} + +
+ + + + {{ documentable.name }} - - - {{ documentable.available_name }} - - - - + {{ t(`CAPTAIN.RESPONSES.DOCUMENTABLE.CONVERSATION`, { id: documentable.display_id, }) }} - - +
- - {{ t(`CAPTAIN.RESPONSES.STATUS.${status.toUpperCase()}`) }} + + {{ timestamp }}
-
- - {{ timestamp }} -
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue index 31e18394f..8d67344e1 100644 --- a/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue +++ b/app/javascript/dashboard/components-next/captain/pageComponents/DeleteDialog.vue @@ -10,6 +10,10 @@ const props = defineProps({ type: String, required: true, }, + translationKey: { + type: String, + required: true, + }, entity: { type: Object, required: true, @@ -25,7 +29,9 @@ const emit = defineEmits(['deleteSuccess']); const { t } = useI18n(); const store = useStore(); const deleteDialogRef = ref(null); -const i18nKey = computed(() => props.type.toUpperCase()); +const i18nKey = computed(() => { + return props.translationKey || props.type.toUpperCase(); +}); const deleteEntity = async payload => { if (!payload) return; diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/AuthConfig.vue b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/AuthConfig.vue new file mode 100644 index 000000000..208a94dba --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/AuthConfig.vue @@ -0,0 +1,73 @@ + + + diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CreateCustomToolDialog.vue b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CreateCustomToolDialog.vue new file mode 100644 index 000000000..0745c6546 --- /dev/null +++ b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/CreateCustomToolDialog.vue @@ -0,0 +1,87 @@ + + +