diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue index a75d1fd6bffe49f539c9fc4ef8996d6364783b14..70e1780aae15280e0f0af374f892ccbb6c56de39 100644 --- a/app/assets/javascripts/super_sidebar/components/help_center.vue +++ b/app/assets/javascripts/super_sidebar/components/help_center.vue @@ -12,7 +12,7 @@ import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility'; import { __ } from '~/locale'; import { STORAGE_KEY } from '~/whats_new/utils/notification'; import Tracking from '~/tracking'; -import { DROPDOWN_Y_OFFSET, HELP_MENU_TRACKING_DEFAULTS } from '../constants'; +import { DROPDOWN_Y_OFFSET, HELP_MENU_TRACKING_DEFAULTS, helpCenterState } from '../constants'; // Left offset required for the dropdown to be aligned with the super sidebar const DROPDOWN_X_OFFSET = -4; @@ -49,6 +49,7 @@ export default { data() { return { showWhatsNewNotification: this.shouldShowWhatsNewNotification(), + helpCenterState, }; }, computed: { @@ -175,9 +176,10 @@ export default { this.$refs.dropdown.close(); }, - async showTanukiBotChat() { - // This will be implemented in the following MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117930 - return true; + showTanukiBotChat() { + this.$refs.dropdown.close(); + + this.helpCenterState.showTanukiBotChatDrawer = true; }, async showWhatsNew() { diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js index 4f5b027c138980fd9776a75a74e5ba77498e81d6..f3f71feaa8a61723b15176f2ff7b7f46731cabdd 100644 --- a/app/assets/javascripts/super_sidebar/constants.js +++ b/app/assets/javascripts/super_sidebar/constants.js @@ -20,6 +20,10 @@ export const sidebarState = Vue.observable({ closePeekTimer: null, }); +export const helpCenterState = Vue.observable({ + showTanukiBotChatDrawer: false, +}); + export const MAX_FREQUENT_PROJECTS_COUNT = 5; export const MAX_FREQUENT_GROUPS_COUNT = 3; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index dba9cafbd719b190dc1582c2283c0a7299c5d892..82da6e959d0668bb4cb939022c2a7eafd7b16089 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -885,6 +885,11 @@ Multi file editor */ $border-color-settings: #e1e1e1; +/* +Drawers +*/ +$wide-drawer: 500px; + /* Modals */ diff --git a/ee/app/assets/javascripts/ai/tanuki_bot/components/app.vue b/ee/app/assets/javascripts/ai/tanuki_bot/components/app.vue new file mode 100644 index 0000000000000000000000000000000000000000..9ed081415fd42af794183357213506b6eb410a8f --- /dev/null +++ b/ee/app/assets/javascripts/ai/tanuki_bot/components/app.vue @@ -0,0 +1,54 @@ +<script> +import { GlDrawer, GlIcon, GlBadge } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; +import { helpCenterState } from '~/super_sidebar/constants'; + +export default { + name: 'TanukiBotChatApp', + i18n: { + tanukiBot: s__('TanukiBot|Tanuki Bot'), + experiment: __('Experiment'), + }, + components: { + GlIcon, + GlDrawer, + GlBadge, + }, + data() { + return { + helpCenterState, + }; + }, + methods: { + closeDrawer() { + this.helpCenterState.showTanukiBotChatDrawer = false; + }, + }, +}; +</script> + +<template> + <section> + <gl-drawer + data-testid="tanuki-bot-chat-drawer" + class="tanuki-bot-chat-drawer gl-reset-line-height" + :z-index="1000" + :open="helpCenterState.showTanukiBotChatDrawer" + @close="closeDrawer" + > + <template #title> + <span class="gl-display-flex gl-align-items-center"> + <gl-icon name="tanuki" class="gl-text-orange-500" /> + <h3 class="gl-my-0 gl-mx-3">{{ $options.i18n.tanukiBot }}</h3> + <gl-badge variant="muted">{{ $options.i18n.experiment }}</gl-badge> + </span> + </template> + </gl-drawer> + <div + v-if="helpCenterState.showTanukiBotChatDrawer" + class="modal-backdrop tanuki-bot-backdrop" + data-testid="tanuki-bot-chat-drawer-backdrop" + @click="closeDrawer" + ></div> + </section> +</template> diff --git a/ee/app/assets/javascripts/ai/tanuki_bot/index.js b/ee/app/assets/javascripts/ai/tanuki_bot/index.js new file mode 100644 index 0000000000000000000000000000000000000000..98505327644c15010a1017a2e9409cc9da674783 --- /dev/null +++ b/ee/app/assets/javascripts/ai/tanuki_bot/index.js @@ -0,0 +1,17 @@ +import Vue from 'vue'; +import TanukiBotChatApp from './components/app.vue'; + +export const initTanukiBotChatDrawer = () => { + const el = document.getElementById('js-tanuki-bot-chat-app'); + + if (!el) { + return false; + } + + return new Vue({ + el, + render(createElement) { + return createElement(TanukiBotChatApp); + }, + }); +}; diff --git a/ee/app/assets/javascripts/main_ee.js b/ee/app/assets/javascripts/main_ee.js index c1fceac6e5a50de4844030cd179973589cf39be6..645b574905477086001cb78437d485e48eb3ff91 100644 --- a/ee/app/assets/javascripts/main_ee.js +++ b/ee/app/assets/javascripts/main_ee.js @@ -1,6 +1,7 @@ import initEETrialBanner from 'ee/ee_trial_banner'; import trackNavbarEvents from 'ee/event_tracking/navbar'; import initNamespaceUserCapReachedAlert from 'ee/namespace_user_cap_reached_alert'; +import { initTanukiBotChatDrawer } from 'ee/ai/tanuki_bot'; if (document.querySelector('.js-verification-reminder') !== null) { // eslint-disable-next-line promise/catch-or-return @@ -14,3 +15,4 @@ initEETrialBanner(); initNamespaceUserCapReachedAlert(); trackNavbarEvents(); +initTanukiBotChatDrawer(); diff --git a/ee/app/assets/stylesheets/components/tanuki_bot_chat.scss b/ee/app/assets/stylesheets/components/tanuki_bot_chat.scss new file mode 100644 index 0000000000000000000000000000000000000000..5e34e0228c0e21ec058d227609effc60ac57696e --- /dev/null +++ b/ee/app/assets/stylesheets/components/tanuki_bot_chat.scss @@ -0,0 +1,15 @@ +.tanuki-bot-chat-drawer { + $height-offset: calc(#{$header-height} + #{$calc-application-bars-height}); + + height: calc(100% - #{$height-offset}); + top: $height-offset !important; // overrides drawerStyles from GlDrawer + bottom: 0; + box-shadow: 0 4px 16px $t-gray-a-24; + overflow-y: hidden; + max-width: $wide-drawer; + width: 100%; +} + +.tanuki-bot-backdrop { + z-index: 999; +} diff --git a/ee/spec/features/tanuki_bot_chat_spec.rb b/ee/spec/features/tanuki_bot_chat_spec.rb index 59cf5b45196c102bf4cc358c9648beba33a870fd..5c505d5daa5328bcf6b0eebca15aeed6bfbbfc2e 100644 --- a/ee/spec/features/tanuki_bot_chat_spec.rb +++ b/ee/spec/features/tanuki_bot_chat_spec.rb @@ -18,10 +18,14 @@ it 'opens a chat drawer to chat with Tanuki Bot' do page.within '[data-testid="super-sidebar"]' do click_button('Help') - find_button('Ask the Tanuki Bot') + click_button('Ask the Tanuki Bot') end - # This spec will be expanded in the following MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/117930 + wait_for_requests + + page.within '[data-testid="tanuki-bot-chat-drawer"]' do + expect(page).to have_text('Tanuki Bot') + end end end end diff --git a/ee/spec/frontend/ai/tanuki_bot/components/app_spec.js b/ee/spec/frontend/ai/tanuki_bot/components/app_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..54210437ac9ddb8e41abaa6e6f440576e70febd4 --- /dev/null +++ b/ee/spec/frontend/ai/tanuki_bot/components/app_spec.js @@ -0,0 +1,67 @@ +import { GlDrawer } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import TanukiBotChatApp from 'ee/ai/tanuki_bot/components/app.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { helpCenterState } from '~/super_sidebar/constants'; + +describe('TanukiBotChatApp', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(TanukiBotChatApp, { + stubs: { + GlDrawer, + Portal: { + template: '<div><slot></slot></div>', + }, + }, + }); + }; + + const findGlDrawer = () => wrapper.findComponent(GlDrawer); + const findGlDrawerBackdrop = () => wrapper.findByTestId('tanuki-bot-chat-drawer-backdrop'); + + describe('GlDrawer interactions', () => { + beforeEach(() => { + createComponent(); + helpCenterState.showTanukiBotChatDrawer = true; + }); + + it('closes the drawer when GlDrawer emits @close', async () => { + findGlDrawer().vm.$emit('close'); + + await nextTick(); + + expect(findGlDrawer().props('open')).toBe(false); + }); + }); + + describe('GlDrawer Backdrop', () => { + beforeEach(() => { + createComponent(); + }); + + it('does not render when drawer is closed', () => { + expect(findGlDrawerBackdrop().exists()).toBe(false); + }); + + describe('when open is true', () => { + beforeEach(() => { + createComponent(); + helpCenterState.showTanukiBotChatDrawer = true; + }); + + it('does render', () => { + expect(findGlDrawerBackdrop().exists()).toBe(true); + }); + + it('when clicked, calls closeDrawer', async () => { + findGlDrawerBackdrop().trigger('click'); + + await nextTick(); + + expect(findGlDrawer().props('open')).toBe(false); + }); + }); + }); +}); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 62e14eebc9a02a695aecfcd7672877ed9097bc64..76393e96fcaa4e4015fdb706cfbeaa2b43edbc64 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -43617,6 +43617,9 @@ msgstr "" msgid "Take a look at the documentation to discover all of GitLab’s capabilities." msgstr "" +msgid "TanukiBot|Tanuki Bot" +msgstr "" + msgid "Target" msgstr "" diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js index aa94ca301db8a71304c8228ef7351b9216a04ff6..b441c5f531da95c16ad031908d4cc645f428a2ef 100644 --- a/spec/frontend/super_sidebar/components/help_center_spec.js +++ b/spec/frontend/super_sidebar/components/help_center_spec.js @@ -7,6 +7,7 @@ import { helpPagePath } from '~/helpers/help_page_helper'; import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { STORAGE_KEY } from '~/whats_new/utils/notification'; +import { helpCenterState } from '~/super_sidebar/constants'; import { mockTracking } from 'helpers/tracking_helper'; import { sidebarData } from '../mock_data'; @@ -99,9 +100,10 @@ describe('HelpCenter component', () => { describe('with show_tanuki_bot true', () => { beforeEach(() => { createWrapper({ ...sidebarData, show_tanuki_bot: true }); + jest.spyOn(wrapper.vm.$refs.dropdown, 'close'); }); - it('shows Ask the Tanuki Bot with the help items', () => { + it('shows Ask the Tanuki Bot with the help items via Portal', () => { expect(findDropdownGroup(0).props('group').items).toEqual([ expect.objectContaining({ icon: 'tanuki', @@ -111,6 +113,20 @@ describe('HelpCenter component', () => { ...DEFAULT_HELP_ITEMS, ]); }); + + describe('when Ask the Tanuki Bot button is clicked', () => { + beforeEach(() => { + findButton('Ask the Tanuki Bot').click(); + }); + + it('closes the dropdown', () => { + expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled(); + }); + + it('sets helpCenterState.showTanukiBotChatDrawer to true', () => { + expect(helpCenterState.showTanukiBotChatDrawer).toBe(true); + }); + }); }); describe('with Gitlab version check feature enabled', () => {