diff --git a/Gemfile b/Gemfile index 40fd33957..a40b0f816 100644 --- a/Gemfile +++ b/Gemfile @@ -109,9 +109,9 @@ gem 'elastic-apm', require: false gem 'newrelic_rpm', require: false gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false gem 'scout_apm', require: false -gem 'sentry-rails', '>= 5.12.0', require: false +gem 'sentry-rails', '>= 5.13.0', require: false gem 'sentry-ruby', require: false -gem 'sentry-sidekiq', '>= 5.12.0', require: false +gem 'sentry-sidekiq', '>= 5.13.0', require: false ##-- background job processing --## gem 'sidekiq', '>= 7.1.3' @@ -200,6 +200,8 @@ group :development do # profiling gem 'rack-mini-profiler', '>= 3.1.1', require: false gem 'stackprof' + # Should install the associated chrome extension to view query logs + gem 'meta_request' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index a6390908b..8c782dd48 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -451,13 +451,16 @@ GEM marcel (1.0.2) maxminddb (0.1.22) memoist (0.16.2) + meta_request (0.7.4) + rack-contrib (>= 1.1, < 3) + railties (>= 3.0.0, < 7.1) method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2023.0218.1) mini_magick (4.12.0) mini_mime (1.1.5) - mini_portile2 (2.8.4) + mini_portile2 (2.8.5) minitest (5.20.0) mock_redis (0.36.0) ruby2_keywords @@ -555,10 +558,12 @@ GEM pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.7.1) + racc (1.7.3) rack (2.2.8) rack-attack (6.7.0) rack (>= 1.0, < 4) + rack-contrib (2.4.0) + rack (< 4) rack-cors (2.0.1) rack (>= 2.0.0) rack-mini-profiler (3.1.1) @@ -599,7 +604,7 @@ GEM thor (~> 1.0) zeitwerk (~> 2.5) rainbow (3.1.1) - rake (13.0.6) + rake (13.1.0) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) @@ -704,13 +709,13 @@ GEM activesupport (>= 4) selectize-rails (0.12.6) semantic_range (3.0.0) - sentry-rails (5.12.0) + sentry-rails (5.13.0) railties (>= 5.0) - sentry-ruby (~> 5.12.0) - sentry-ruby (5.12.0) + sentry-ruby (~> 5.13.0) + sentry-ruby (5.13.0) concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.12.0) - sentry-ruby (~> 5.12.0) + sentry-sidekiq (5.13.0) + sentry-ruby (~> 5.13.0) sidekiq (>= 3.0) sexp_processor (4.17.0) shoulda-matchers (5.3.0) @@ -760,7 +765,7 @@ GEM stripe (8.5.0) telephone_number (1.4.20) test-prof (1.2.1) - thor (1.2.2) + thor (1.3.0) tilt (2.2.0) time_diff (0.3.0) activesupport @@ -895,6 +900,7 @@ DEPENDENCIES listen lograge (~> 0.14.0) maxminddb + meta_request mock_redis neighbor newrelic-sidekiq-metrics (>= 1.6.2) @@ -929,9 +935,9 @@ DEPENDENCIES scout_apm scss_lint seed_dump - sentry-rails (>= 5.12.0) + sentry-rails (>= 5.13.0) sentry-ruby - sentry-sidekiq (>= 5.12.0) + sentry-sidekiq (>= 5.13.0) shoulda-matchers sidekiq (>= 7.1.3) sidekiq-cron (>= 1.11.0) diff --git a/README.md b/README.md index ac11d649d..11ac5dc2c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -

- Woot-logo +Chat dashboard dark mode +Chat dashboard -

Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.

-

+___ -

+# Chatwoot + +Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc. +

Deploy @@ -13,9 +15,7 @@

-___ - -

+

Maintainability CircleCI Badge Docker Pull Badge @@ -29,9 +29,8 @@ ___ Artifact HUB

-Chat dashboard - - +Chat dashboard dark mode +Chat dashboard Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile. diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb index 6d2a181f0..bd8da5549 100644 --- a/app/controllers/api/v1/accounts/portals_controller.rb +++ b/app/controllers/api/v1/accounts/portals_controller.rb @@ -47,6 +47,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController head :ok end + def logo + @portal.logo.purge if @portal.logo.attached? + head :ok + end + def process_attached_logo blob_id = params[:blob_id] blob = ActiveStorage::Blob.find_by(id: blob_id) diff --git a/app/javascript/dashboard/api/helpCenter/portals.js b/app/javascript/dashboard/api/helpCenter/portals.js index 28220b0b5..7c6210dbd 100644 --- a/app/javascript/dashboard/api/helpCenter/portals.js +++ b/app/javascript/dashboard/api/helpCenter/portals.js @@ -17,6 +17,10 @@ class PortalsAPI extends ApiClient { deletePortal(portalSlug) { return axios.delete(`${this.url}/${portalSlug}`); } + + deleteLogo(portalSlug) { + return axios.delete(`${this.url}/${portalSlug}/logo`); + } } export default PortalsAPI; diff --git a/app/javascript/dashboard/assets/scss/_formulate.scss b/app/javascript/dashboard/assets/scss/_formulate.scss index 57c43a3d6..d683ce3c7 100644 --- a/app/javascript/dashboard/assets/scss/_formulate.scss +++ b/app/javascript/dashboard/assets/scss/_formulate.scss @@ -16,3 +16,23 @@ width: 100%; } } + +.integration-hooks { + .formulate-input[data-type='checkbox'] { + .formulate-input-wrapper { + @apply flex; + + .formulate-input-element { + @apply pr-2; + + input { + @apply mb-0; + } + } + } + + .formulate-input-element-decorator { + @apply hidden; + } + } +} diff --git a/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue b/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue index ab4047a1e..2d7951b05 100644 --- a/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue +++ b/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue @@ -61,6 +61,7 @@ export default { computed: { ...mapGetters({ currentChat: 'getSelectedChat', + isAChatwootInstance: 'globalConfig/isAChatwootInstance', }), isAICTAModalDismissed() { return this.uiSettings.is_open_ai_cta_modal_dismissed; @@ -70,7 +71,8 @@ export default { return ( this.isAdmin && !this.isAIIntegrationEnabled && - !this.isAICTAModalDismissed + !this.isAICTAModalDismissed && + this.isAChatwootInstance ); }, // Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal. diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index 38bb24f8f..10b1dce32 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -114,7 +114,6 @@ import { LocalStorage } from 'shared/helpers/localStorage'; // constants import { BUS_EVENTS } from 'shared/constants/busEvents'; -import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { REPLY_POLICY } from 'shared/constants/links'; import wootConstants from 'dashboard/constants/globals'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; @@ -285,14 +284,6 @@ export default { return this.currentChat.unread_count || 0; }, inboxSupportsReplyTo() { - if ( - !this.isFeatureEnabledonAccount( - this.accountId, - FEATURE_FLAGS.MESSAGE_REPLY_TO - ) - ) { - return {}; - } return { incoming: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO), outgoing: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING), @@ -349,7 +340,7 @@ export default { // method available in mixin, need to ensure that integrations are present await this.fetchIntegrationsIfRequired(); - if (!this.isAIIntegrationEnabled) { + if (!this.isLabelSuggestionFeatureEnabled) { return; } diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 37c34a031..5e1f34904 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -194,7 +194,6 @@ import { replaceSignature, extractTextFromMarkdown, } from 'dashboard/helper/editorHelper'; -import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; import { LocalStorage } from 'shared/helpers/localStorage'; @@ -276,11 +275,7 @@ export default { return ( this.inReplyTo?.id && !this.isPrivate && - this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) && - this.isFeatureEnabledonAccount( - this.accountId, - FEATURE_FLAGS.MESSAGE_REPLY_TO - ) + this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) ); }, showRichContentEditor() { diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue index 4ee7aaa9d..416af5d06 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue @@ -192,7 +192,7 @@ export default { if (!this.showStatusIndicators) { return false; } - + // Messages will be marked as sent for the Email channel if they have a source ID. if (this.isAnEmailChannel) { return !!this.sourceId; } @@ -201,11 +201,12 @@ export default { this.isAWhatsAppChannel || this.isATwilioChannel || this.isAFacebookInbox || - this.isASmsInbox + this.isASmsInbox || + this.isATelegramChannel ) { return this.sourceId && this.isSent; } - // There is no source id for the line channel + // All messages will be mark as sent for the Line channel, as there is no source ID. if (this.isALineChannel) { return true; } @@ -216,15 +217,15 @@ export default { if (!this.showStatusIndicators) { return false; } - if ( this.isAWhatsAppChannel || this.isATwilioChannel || - this.isASmsInbox + this.isASmsInbox || + this.isAFacebookInbox ) { return this.sourceId && this.isDelivered; } - // We will consider messages as delivered for web widget inbox and API inbox if they are sent + // All messages marked as delivered for the web widget inbox and API inbox once they are sent. if (this.isAWebWidgetInbox || this.isAPIInbox) { return this.isSent; } @@ -238,12 +239,6 @@ export default { if (!this.showStatusIndicators) { return false; } - - if (this.isAWebWidgetInbox || this.isAPIInbox) { - const { contact_last_seen_at: contactLastSeenAt } = this.currentChat; - return contactLastSeenAt >= this.createdAt; - } - if ( this.isAWhatsAppChannel || this.isATwilioChannel || @@ -252,6 +247,10 @@ export default { return this.sourceId && this.isRead; } + if (this.isAWebWidgetInbox || this.isAPIInbox) { + return this.isRead; + } + return false; }, }, diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js index 143ee521d..4280e0f10 100644 --- a/app/javascript/dashboard/featureFlags.js +++ b/app/javascript/dashboard/featureFlags.js @@ -16,6 +16,5 @@ export const FEATURE_FLAGS = { TEAM_MANAGEMENT: 'team_management', VOICE_RECORDER: 'voice_recorder', AUDIT_LOGS: 'audit_logs', - MESSAGE_REPLY_TO: 'message_reply_to', INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply', }; diff --git a/app/javascript/dashboard/helper/URLHelper.js b/app/javascript/dashboard/helper/URLHelper.js index 7ea8531d3..5464d1107 100644 --- a/app/javascript/dashboard/helper/URLHelper.js +++ b/app/javascript/dashboard/helper/URLHelper.js @@ -96,3 +96,15 @@ export const getArticleSearchURL = ({ return `${host}/${portalSlug}/articles?${queryParams.toString()}`; }; + +export const hasValidAvatarUrl = avatarUrl => { + try { + const { host: avatarUrlHost } = new URL(avatarUrl); + const isFromGravatar = ['www.gravatar.com', 'gravatar'].includes( + avatarUrlHost + ); + return avatarUrl && !isFromGravatar; + } catch (error) { + return false; + } +}; diff --git a/app/javascript/dashboard/helper/automationHelper.js b/app/javascript/dashboard/helper/automationHelper.js index f186b0e92..d1a85570b 100644 --- a/app/javascript/dashboard/helper/automationHelper.js +++ b/app/javascript/dashboard/helper/automationHelper.js @@ -135,6 +135,7 @@ export const getActionOptions = ({ agents, teams, labels, type }) => { assign_team: teams, send_email_to_team: teams, add_label: generateConditionOptions(labels, 'title'), + remove_label: generateConditionOptions(labels, 'title'), change_priority: PRIORITY_CONDITION_VALUES, }; return actionsMap[type]; diff --git a/app/javascript/dashboard/helper/specs/URLHelper.spec.js b/app/javascript/dashboard/helper/specs/URLHelper.spec.js index 1b5814556..d137c367c 100644 --- a/app/javascript/dashboard/helper/specs/URLHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/URLHelper.spec.js @@ -4,6 +4,7 @@ import { isValidURL, conversationListPageURL, getArticleSearchURL, + hasValidAvatarUrl, } from '../URLHelper'; describe('#URL Helpers', () => { @@ -164,4 +165,29 @@ describe('#URL Helpers', () => { expect(url).toBe('myurl.com/news/articles?page=1&locale=en'); }); }); + + describe('hasValidAvatarUrl', () => { + test('should return true for valid non-Gravatar URL', () => { + expect(hasValidAvatarUrl('https://chatwoot.com/avatar.jpg')).toBe(true); + }); + + test('should return false for a Gravatar URL (www.gravatar.com)', () => { + expect(hasValidAvatarUrl('https://www.gravatar.com/avatar.jpg')).toBe( + false + ); + }); + + test('should return false for a Gravatar URL (gravatar)', () => { + expect(hasValidAvatarUrl('https://gravatar/avatar.jpg')).toBe(false); + }); + + test('should handle invalid URL', () => { + expect(hasValidAvatarUrl('invalid-url')).toBe(false); // or expect an error, depending on function design + }); + + test('should return false for empty or undefined URL', () => { + expect(hasValidAvatarUrl('')).toBe(false); + expect(hasValidAvatarUrl()).toBe(false); + }); + }); }); diff --git a/app/javascript/dashboard/i18n/locale/en/helpCenter.json b/app/javascript/dashboard/i18n/locale/en/helpCenter.json index 43b419328..6d4a3eb8c 100644 --- a/app/javascript/dashboard/i18n/locale/en/helpCenter.json +++ b/app/javascript/dashboard/i18n/locale/en/helpCenter.json @@ -223,7 +223,10 @@ "LOGO": { "LABEL": "Logo", "UPLOAD_BUTTON": "Upload logo", - "HELP_TEXT": "This logo will be displayed on the portal header." + "HELP_TEXT": "This logo will be displayed on the portal header.", + "IMAGE_UPLOAD_SUCCESS": "Logo uploaded successfully", + "IMAGE_UPLOAD_ERROR": "Logo deleted successfully", + "IMAGE_DELETE_ERROR": "Error while deleting logo" }, "NAME": { "LABEL": "Name", diff --git a/app/javascript/dashboard/mixins/aiMixin.js b/app/javascript/dashboard/mixins/aiMixin.js index 3a9fc4a75..54bde3ffb 100644 --- a/app/javascript/dashboard/mixins/aiMixin.js +++ b/app/javascript/dashboard/mixins/aiMixin.js @@ -15,18 +15,26 @@ export default { currentChat: 'getSelectedChat', replyMode: 'draftMessages/getReplyEditorMode', }), - isAIIntegrationEnabled() { - return !!this.appIntegrations.find( + aiIntegration() { + return this.appIntegrations.find( integration => integration.id === 'openai' && !!integration.hooks.length - ); + )?.hooks[0]; + }, + isAIIntegrationEnabled() { + return !!this.aiIntegration; + }, + isLabelSuggestionFeatureEnabled() { + if (this.aiIntegration) { + const { settings = {} } = this.aiIntegration || {}; + return settings.label_suggestion; + } + return false; }, isFetchingAppIntegrations() { return this.uiFlags.isFetching; }, hookId() { - return this.appIntegrations.find( - integration => integration.id === 'openai' && !!integration.hooks.length - ).hooks[0].id; + return this.aiIntegration.id; }, draftMessage() { return this.$store.getters['draftMessages/get'](this.draftKey); diff --git a/app/javascript/dashboard/modules/widget-preview/components/WidgetFooter.vue b/app/javascript/dashboard/modules/widget-preview/components/WidgetFooter.vue index 64355b6cd..b5d4bbc28 100644 --- a/app/javascript/dashboard/modules/widget-preview/components/WidgetFooter.vue +++ b/app/javascript/dashboard/modules/widget-preview/components/WidgetFooter.vue @@ -12,6 +12,7 @@
-
- - - {{ $t('HELP_CENTER.PORTAL.ADD.LOGO.UPLOAD_BUTTON') }} - + +
+ + {{ $t('PROFILE_SETTINGS.DELETE_AVATAR') }} + +

import { required, minLength } from 'vuelidate/lib/validators'; import { isDomain } from 'shared/helpers/Validators'; -import thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; + +import alertMixin from 'shared/mixins/alertMixin'; import { convertToCategorySlug } from 'dashboard/helper/commons.js'; import { buildPortalURL } from 'dashboard/helper/portalHelper'; import wootConstants from 'dashboard/constants/globals'; +import { hasValidAvatarUrl } from 'dashboard/helper/URLHelper'; +import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; +import { uploadFile } from 'dashboard/helper/uploadHelper'; const { EXAMPLE_URL } = wootConstants; +const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB export default { - components: { - thumbnail, - }, + mixins: [alertMixin], props: { portal: { type: Object, @@ -118,6 +124,10 @@ export default { slug: '', domain: '', alertMessage: '', + + // Logouploader keys + avatarBlobId: '', + logoUrl: '', }; }, validations: { @@ -159,6 +169,9 @@ export default { exampleURL: EXAMPLE_URL, }); }, + showDeleteButton() { + return hasValidAvatarUrl(this.logoUrl); + }, }, mounted() { const portal = this.portal || {}; @@ -166,6 +179,13 @@ export default { this.slug = portal.slug || ''; this.domain = portal.custom_domain || ''; this.alertMessage = ''; + if (portal.logo) { + const { + logo: { file_url: logoURL, blob_id: blobId }, + } = portal; + this.logoUrl = logoURL; + this.avatarBlobId = blobId; + } }, methods: { onNameChange() { @@ -181,9 +201,44 @@ export default { name: this.name, slug: this.slug, custom_domain: this.domain, + blob_id: this.avatarBlobId || null, }; this.$emit('submit', portal); }, + async deleteAvatar() { + this.logoUrl = ''; + this.avatarBlobId = ''; + this.$emit('delete-logo'); + }, + onFileChange({ file }) { + if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) { + this.uploadLogoToStorage(file); + } else { + this.showAlert( + this.$t( + 'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR', + { + size: MAXIMUM_FILE_UPLOAD_SIZE, + } + ) + ); + } + + this.$refs.imageUpload.value = ''; + }, + async uploadLogoToStorage(file) { + try { + const { fileUrl, blobId } = await uploadFile(file); + if (fileUrl) { + this.logoUrl = fileUrl; + this.avatarBlobId = blobId; + } + } catch (error) { + this.showAlert( + this.$t('HELP_CENTER.PORTAL.ADD.LOGO.IMAGE_DELETE_ERROR') + ); + } + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalBasic.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalBasic.vue index c6d9f8b15..8e3579f02 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalBasic.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/EditPortalBasic.vue @@ -7,6 +7,7 @@ $t('HELP_CENTER.PORTAL.EDIT.EDIT_BASIC_INFO.BUTTON_TEXT') " @submit="updatePortalSettings" + @delete-logo="deleteLogo" /> @@ -70,6 +71,19 @@ export default { this.showAlert(this.alertMessage); } }, + async deleteLogo() { + try { + const portalSlug = this.lastPortalSlug; + await this.$store.dispatch('portals/deleteLogo', { + portalSlug, + }); + } catch (error) { + this.alertMessage = + error?.message || + this.$t('HELP_CENTER.PORTAL.ADD.LOGO.IMAGE_DELETE_ERROR'); + this.showAlert(this.alertMessage); + } + }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalDetails.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalDetails.vue index 98aaf544f..2857867bb 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalDetails.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalDetails.vue @@ -40,9 +40,12 @@ export default { methods: { async createPortal(portal) { try { + const { blob_id: blobId } = portal; await this.$store.dispatch('portals/create', { portal, + blob_id: blobId, }); + this.alertMessage = this.$t( 'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_BASIC' ); diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js index 9fcee3f45..e86aa5161 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/constants.js @@ -67,6 +67,11 @@ export const AUTOMATIONS = { name: 'Add a label', attributeI18nKey: 'ADD_LABEL', }, + { + key: 'remove_label', + name: 'Remove a label', + attributeI18nKey: 'REMOVE_LABEL', + }, { key: 'send_email_to_team', name: 'Send an email to team', @@ -556,6 +561,11 @@ export const AUTOMATION_ACTION_TYPES = [ label: 'Add a label', inputType: 'multi_select', }, + { + key: 'remove_label', + label: 'Remove a label', + inputType: 'multi_select', + }, { key: 'send_email_to_team', label: 'Send an email to team', diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrationapps/NewHook.vue b/app/javascript/dashboard/routes/dashboard/settings/integrationapps/NewHook.vue index 93eed8795..454f03dac 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/integrationapps/NewHook.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/integrationapps/NewHook.vue @@ -1,6 +1,6 @@