From afff4cbed8b0d1fea836938a7e6b6d22ee92981a Mon Sep 17 00:00:00 2001 From: Elwyn Benson Date: Wed, 4 Sep 2024 18:09:31 +1200 Subject: [PATCH] fix: update context-items-menu components with updated schema - AiContextItem schema is now finalised in the Language Server - Update all components, tests, stories to use the new expected structure - Add handling for missing `title`, which is now optional. - Update validators to match the type definitions --- .../components/duo_chat_context/constants.js | 6 +- .../duo_chat_context_item_menu.spec.js | 24 ++-- .../duo_chat_context_item_menu.vue | 8 +- ...chat_context_item_menu_search_item.spec.js | 14 +- ...duo_chat_context_item_menu_search_item.vue | 20 +-- ...hat_context_item_menu_search_items.spec.js | 25 ++-- ...uo_chat_context_item_menu_search_items.vue | 11 +- .../duo_chat_context_item_popover.spec.js | 16 +-- .../duo_chat_context_item_popover.stories.js | 4 +- .../duo_chat_context_item_popover.vue | 48 +++---- .../duo_chat_context_item_selections.vue | 20 +-- .../duo_chat_context/mock_context_data.js | 131 ++++++++---------- .../chat/components/duo_chat_context/utils.js | 8 +- .../components/duo_chat_context/utils.spec.js | 94 +++++++++++-- .../experimental/duo/chat/duo_chat.stories.js | 4 +- 15 files changed, 248 insertions(+), 185 deletions(-) diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/constants.js b/src/components/experimental/duo/chat/components/duo_chat_context/constants.js index d1d1b777db..7d825c7cc9 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/constants.js +++ b/src/components/experimental/duo/chat/components/duo_chat_context/constants.js @@ -1,3 +1,3 @@ -export const CONTEXT_ITEM_TYPE_ISSUE = 'issue'; -export const CONTEXT_ITEM_TYPE_MERGE_REQUEST = 'merge_request'; -export const CONTEXT_ITEM_TYPE_PROJECT_FILE = 'project_file'; +export const CONTEXT_ITEM_CATEGORY_ISSUE = 'issue'; +export const CONTEXT_ITEM_CATEGORY_MERGE_REQUEST = 'merge_request'; +export const CONTEXT_ITEM_CATEGORY_FILE = 'file'; diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.spec.js b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.spec.js index 9fb517fda5..670067ddf2 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.spec.js +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.spec.js @@ -2,9 +2,9 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { getMockCategory, getMockContextItems, MOCK_CATEGORIES } from '../mock_context_data'; import { - CONTEXT_ITEM_TYPE_ISSUE, - CONTEXT_ITEM_TYPE_MERGE_REQUEST, - CONTEXT_ITEM_TYPE_PROJECT_FILE, + CONTEXT_ITEM_CATEGORY_ISSUE, + CONTEXT_ITEM_CATEGORY_MERGE_REQUEST, + CONTEXT_ITEM_CATEGORY_FILE, } from '../constants'; import GlDuoChatContextItemSelections from '../duo_chat_context_item_selections/duo_chat_context_item_selections.vue'; import GlDuoChatContextItemMenuCategoryItems from './duo_chat_context_item_menu_category_items.vue'; @@ -122,19 +122,22 @@ describe('GlDuoChatContextItemMenu', () => { }); describe.each([ - CONTEXT_ITEM_TYPE_ISSUE, - CONTEXT_ITEM_TYPE_MERGE_REQUEST, - CONTEXT_ITEM_TYPE_PROJECT_FILE, + CONTEXT_ITEM_CATEGORY_ISSUE, + CONTEXT_ITEM_CATEGORY_MERGE_REQUEST, + CONTEXT_ITEM_CATEGORY_FILE, ])('when a "%s" category has been selected', (categoryValue) => { let category; let results; beforeEach(() => { category = getMockCategory(categoryValue); results = getMockContextItems() - .filter((item) => item.type === categoryValue) + .filter((item) => item.category === categoryValue) .map((item, index) => ({ ...item, - isEnabled: index % 2 === 0, // disable odd indexed items + metadata: { + ...item.metadata, + enabled: index % 2 === 0, // disable odd indexed items + }, })); createComponent({ @@ -260,7 +263,10 @@ describe('GlDuoChatContextItemMenu', () => { await wrapper.setProps({ results: results.map((result, index) => ({ ...result, - isEnabled: index === firstEnabledIndex, + metadata: { + ...result.metadata, + enabled: index === firstEnabledIndex, + }, })), }); await nextTick(); diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.vue b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.vue index 65093aea2c..a3fbca3a2b 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.vue +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.vue @@ -77,7 +77,7 @@ export default { return this.open && !this.selectedCategory; }, allResultsAreDisabled() { - return this.results.every((result) => !result.isEnabled); + return this.results.every((result) => !result.metadata.enabled); }, }, watch: { @@ -90,7 +90,7 @@ export default { this.debouncedSearch(query); }, results(newResults) { - const firstEnabledIndex = newResults.findIndex((result) => result.isEnabled); + const firstEnabledIndex = newResults.findIndex((result) => result.metadata.enabled); this.activeIndex = firstEnabledIndex >= 0 ? firstEnabledIndex : 0; }, }, @@ -117,7 +117,7 @@ export default { }); }, SEARCH_DEBOUNCE_MS), selectItem(item) { - if (!item.isEnabled) { + if (!item.metadata.enabled) { return; } @@ -214,7 +214,7 @@ export default { // If we've looped through all items and found no enabled ones, keep the current index return; } - } while (!this.results[newIndex].isEnabled); + } while (!this.results[newIndex].metadata.enabled); this.activeIndex = newIndex; }, diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.spec.js b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.spec.js index 9ddf410b25..4c5eacfbca 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.spec.js +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.spec.js @@ -6,9 +6,9 @@ import { MOCK_CONTEXT_ITEM_MERGE_REQUEST, } from '../mock_context_data'; import { - CONTEXT_ITEM_TYPE_ISSUE, - CONTEXT_ITEM_TYPE_MERGE_REQUEST, - CONTEXT_ITEM_TYPE_PROJECT_FILE, + CONTEXT_ITEM_CATEGORY_ISSUE, + CONTEXT_ITEM_CATEGORY_MERGE_REQUEST, + CONTEXT_ITEM_CATEGORY_FILE, } from '../constants'; import GlDuoChatContextItemPopover from '../duo_chat_context_item_popover/duo_chat_context_item_popover.vue'; import GlDuoChatContextItemMenuSearchItem from './duo_chat_context_item_menu_search_item.vue'; @@ -30,15 +30,15 @@ describe('GlDuoChatContextItemMenuContextSearchItem', () => { describe.each([ { - category: getMockCategory(CONTEXT_ITEM_TYPE_PROJECT_FILE), + category: getMockCategory(CONTEXT_ITEM_CATEGORY_FILE), contextItem: MOCK_CONTEXT_ITEM_FILE, }, { - category: getMockCategory(CONTEXT_ITEM_TYPE_ISSUE), + category: getMockCategory(CONTEXT_ITEM_CATEGORY_ISSUE), contextItem: MOCK_CONTEXT_ITEM_ISSUE, }, { - category: getMockCategory(CONTEXT_ITEM_TYPE_MERGE_REQUEST), + category: getMockCategory(CONTEXT_ITEM_CATEGORY_MERGE_REQUEST), contextItem: MOCK_CONTEXT_ITEM_MERGE_REQUEST, }, ])('for "$category"', ({ category, contextItem }) => { @@ -58,7 +58,7 @@ describe('GlDuoChatContextItemMenuContextSearchItem', () => { }); it('renders the default context item title', () => { - expect(wrapper.text()).toContain(contextItem.metadata.name); + expect(wrapper.text()).toContain(contextItem.metadata.title); }); }); }); diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.vue b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.vue index df4175b159..fe45351990 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.vue +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_item.vue @@ -8,9 +8,9 @@ import { formatMergeRequestId, } from '../utils'; import { - CONTEXT_ITEM_TYPE_ISSUE, - CONTEXT_ITEM_TYPE_MERGE_REQUEST, - CONTEXT_ITEM_TYPE_PROJECT_FILE, + CONTEXT_ITEM_CATEGORY_ISSUE, + CONTEXT_ITEM_CATEGORY_MERGE_REQUEST, + CONTEXT_ITEM_CATEGORY_FILE, } from '../constants'; export default { @@ -30,16 +30,16 @@ export default { }, computed: { title() { - return this.contextItem.metadata?.name || ''; + return this.contextItem.metadata?.title || ''; }, secondaryText() { switch (this.category.value) { - case CONTEXT_ITEM_TYPE_PROJECT_FILE: - return this.contextItem.metadata.info.relFilePath; - case CONTEXT_ITEM_TYPE_ISSUE: - return formatIssueId(this.contextItem.metadata.info.iid); - case CONTEXT_ITEM_TYPE_MERGE_REQUEST: - return formatMergeRequestId(this.contextItem.metadata.info.iid); + case CONTEXT_ITEM_CATEGORY_FILE: + return this.contextItem.metadata.relativePath; + case CONTEXT_ITEM_CATEGORY_ISSUE: + return formatIssueId(this.contextItem.metadata.iid); + case CONTEXT_ITEM_CATEGORY_MERGE_REQUEST: + return formatMergeRequestId(this.contextItem.metadata.iid); default: return ''; } diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.spec.js b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.spec.js index 8fca41335a..4757d463d9 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.spec.js +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.spec.js @@ -2,9 +2,9 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { MOCK_CATEGORIES, getMockContextItems, getMockCategory } from '../mock_context_data'; import { - CONTEXT_ITEM_TYPE_ISSUE, - CONTEXT_ITEM_TYPE_MERGE_REQUEST, - CONTEXT_ITEM_TYPE_PROJECT_FILE, + CONTEXT_ITEM_CATEGORY_ISSUE, + CONTEXT_ITEM_CATEGORY_MERGE_REQUEST, + CONTEXT_ITEM_CATEGORY_FILE, } from '../constants'; import GlDuoChatContextItemMenuSearchItems from './duo_chat_context_item_menu_search_items.vue'; import GlDuoChatContextItemMenuSearchItemsLoading from './duo_chat_context_item_menu_search_items_loading.vue'; @@ -17,7 +17,8 @@ describe('GlDuoChatContextItemMenuSearchItems', () => { const createWrapper = (props = {}) => { category = props.category || MOCK_CATEGORIES.at(0); - results = props.results || getMockContextItems().filter((item) => item.type === category.value); + results = + props.results || getMockContextItems().filter((item) => item.category === category.value); wrapper = shallowMount(GlDuoChatContextItemMenuSearchItems, { propsData: { @@ -196,7 +197,10 @@ describe('GlDuoChatContextItemMenuSearchItems', () => { await wrapper.setProps({ results: results.map((result, index) => ({ ...result, - isEnabled: index !== disabledIndex, + metadata: { + ...result.metadata, + enabled: index !== disabledIndex, + }, })), }); @@ -218,7 +222,10 @@ describe('GlDuoChatContextItemMenuSearchItems', () => { wrapper.setProps({ results: getMockContextItems().map((result) => ({ ...result, - isEnabled: false, + metadata: { + ...result.metadata, + enabled: false, + }, })), }); await nextTick(); @@ -231,15 +238,15 @@ describe('GlDuoChatContextItemMenuSearchItems', () => { describe.each([ { - testCase: getMockCategory(CONTEXT_ITEM_TYPE_PROJECT_FILE), + testCase: getMockCategory(CONTEXT_ITEM_CATEGORY_FILE), expectedPlaceholder: 'Search files...', }, { - testCase: getMockCategory(CONTEXT_ITEM_TYPE_ISSUE), + testCase: getMockCategory(CONTEXT_ITEM_CATEGORY_ISSUE), expectedPlaceholder: 'Search issues...', }, { - testCase: getMockCategory(CONTEXT_ITEM_TYPE_MERGE_REQUEST), + testCase: getMockCategory(CONTEXT_ITEM_CATEGORY_MERGE_REQUEST), expectedPlaceholder: 'Search merge requests...', }, ])('when category is "$testCase.label"', ({ testCase, expectedPlaceholder }) => { diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.vue b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.vue index 6349ca7ee6..ca20fdb897 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.vue +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu_search_items.vue @@ -87,12 +87,12 @@ export default { this.$emit('keyup', e); }, setActiveIndex(index) { - if (this.results[index]?.isEnabled) { + if (this.results[index]?.metadata.enabled) { this.$emit('active-index-change', index); } }, isActiveItem(contextItem, index) { - return index === this.activeIndex && contextItem.isEnabled; + return index === this.activeIndex && contextItem.metadata.enabled; }, }, i18n: { @@ -127,9 +127,10 @@ export default { :key="contextItem.id" :class="{ 'active-command': isActiveItem(contextItem, index), - 'gl-cursor-not-allowed [&>button]:focus-within:!gl-shadow-none': !contextItem.isEnabled, + 'gl-cursor-not-allowed [&>button]:focus-within:!gl-shadow-none': + !contextItem.metadata.enabled, }" - :tabindex="!contextItem.isEnabled ? -1 : undefined" + :tabindex="!contextItem.metadata.enabled ? -1 : undefined" class="duo-chat-context-search-result-item" data-testid="search-result-item" @click="selectItem(contextItem)" @@ -138,7 +139,7 @@ export default { diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.spec.js b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.spec.js index ca6f7261a9..822690b68c 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.spec.js +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.spec.js @@ -43,7 +43,7 @@ describe('GlDuoChatContextItemPopover', () => { expect(popover.attributes('target')).toBe('test-target'); expect(popover.props('triggers')).toBe('hover focus'); expect(popover.props('placement')).toBe('top'); - expect(popover.props('title')).toBe(MOCK_CONTEXT_ITEM_FILE.metadata.name); + expect(popover.props('title')).toBe(MOCK_CONTEXT_ITEM_FILE.metadata.title); }); it('renders the item name in the title slot', () => { @@ -59,23 +59,23 @@ describe('GlDuoChatContextItemPopover', () => { } ); - expect(findPopoverTitle().text()).toBe(MOCK_CONTEXT_ITEM_FILE.metadata.name); + expect(findPopoverTitle().text()).toBe(MOCK_CONTEXT_ITEM_FILE.metadata.title); }); describe('item info rendering', () => { it.each([ - ['file', MOCK_CONTEXT_ITEM_FILE, MOCK_CONTEXT_ITEM_FILE.metadata.info.relFilePath], - ['issue', MOCK_CONTEXT_ITEM_ISSUE, MOCK_CONTEXT_ITEM_ISSUE.metadata.info.iid.toString()], + ['file', MOCK_CONTEXT_ITEM_FILE, MOCK_CONTEXT_ITEM_FILE.metadata.relativePath], + ['issue', MOCK_CONTEXT_ITEM_ISSUE, MOCK_CONTEXT_ITEM_ISSUE.metadata.iid.toString()], [ 'merge request', MOCK_CONTEXT_ITEM_MERGE_REQUEST, - MOCK_CONTEXT_ITEM_MERGE_REQUEST.metadata.info.iid.toString(), + MOCK_CONTEXT_ITEM_MERGE_REQUEST.metadata.iid.toString(), ], ])('renders correct project and type for %s', (_, contextItem, expected) => { createComponent({ contextItem }); const content = findPopover().text(); - expect(content).toContain(contextItem.metadata.info.project); + expect(content).toContain(contextItem.metadata.project); expect(content).toContain(expected); }); @@ -83,7 +83,7 @@ describe('GlDuoChatContextItemPopover', () => { createComponent({ contextItem: MOCK_CONTEXT_ITEM_FILE }); const content = findPopover().text(); - expect(content).toContain(MOCK_CONTEXT_ITEM_FILE.metadata.info.relFilePath); + expect(content).toContain(MOCK_CONTEXT_ITEM_FILE.metadata.relativePath); }); it.each([ @@ -109,7 +109,7 @@ describe('GlDuoChatContextItemPopover', () => { it('renders default disabled message when no specific reasons are provided', () => { const itemWithoutReasons = { ...MOCK_CONTEXT_ITEM_FILE_DISABLED, - info: { ...MOCK_CONTEXT_ITEM_FILE_DISABLED.metadata.info, disabledReasons: undefined }, + metadata: { ...MOCK_CONTEXT_ITEM_FILE_DISABLED.metadata, disabledReasons: undefined }, }; createComponent({ contextItem: itemWithoutReasons }); diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.stories.js b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.stories.js index 2cf9d2643a..faaf10d6f5 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.stories.js +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.stories.js @@ -22,10 +22,10 @@ const Template = (args, { argTypes }) => ({ template: `
diff --git a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.vue b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.vue index b8a11e168f..065a3176e5 100644 --- a/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.vue +++ b/src/components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.vue @@ -3,9 +3,9 @@ import GlPopover from '../../../../../../base/popover/popover.vue'; import GlIcon from '../../../../../../base/icon/icon.vue'; import { translate } from '../../../../../../../utils/i18n'; import { - CONTEXT_ITEM_TYPE_ISSUE, - CONTEXT_ITEM_TYPE_MERGE_REQUEST, - CONTEXT_ITEM_TYPE_PROJECT_FILE, + CONTEXT_ITEM_CATEGORY_ISSUE, + CONTEXT_ITEM_CATEGORY_MERGE_REQUEST, + CONTEXT_ITEM_CATEGORY_FILE, } from '../constants'; import { formatIssueId, formatMergeRequestId } from '../utils'; import GlAlert from '../../../../../../base/alert/alert.vue'; @@ -42,44 +42,44 @@ export default { }, }, computed: { - itemInfo() { - return this.contextItem.metadata?.info || {}; - }, id() { const isIssuable = - this.contextItem.type === CONTEXT_ITEM_TYPE_ISSUE || - this.contextItem.type === CONTEXT_ITEM_TYPE_MERGE_REQUEST; - return isIssuable ? this.itemInfo.iid || '' : null; + this.contextItem.category === CONTEXT_ITEM_CATEGORY_ISSUE || + this.contextItem.category === CONTEXT_ITEM_CATEGORY_MERGE_REQUEST; + return isIssuable ? this.contextItem.metadata.iid : null; }, formattedId() { - switch (this.contextItem.type) { - case CONTEXT_ITEM_TYPE_ISSUE: + switch (this.contextItem.category) { + case CONTEXT_ITEM_CATEGORY_ISSUE: return formatIssueId(this.id); - case CONTEXT_ITEM_TYPE_MERGE_REQUEST: + case CONTEXT_ITEM_CATEGORY_MERGE_REQUEST: return formatMergeRequestId(this.id); default: return ''; } }, + title() { + return this.contextItem.metadata.title || ''; + }, filePath() { - return this.contextItem.type === CONTEXT_ITEM_TYPE_PROJECT_FILE - ? this.itemInfo.relFilePath || '' + return this.contextItem.category === CONTEXT_ITEM_CATEGORY_FILE + ? this.contextItem.metadata.relativePath || '' : null; }, filePathArray() { return this.filePath?.split('/'); }, isEnabled() { - return this.contextItem.isEnabled !== false; + return this.contextItem.metadata.enabled !== false; }, disabledMessage() { - return Array.isArray(this.contextItem.disabledReasons) && - this.contextItem.disabledReasons.length > 0 - ? this.contextItem.disabledReasons.join(', ') + return Array.isArray(this.contextItem.metadata.disabledReasons) && + this.contextItem.metadata.disabledReasons.length > 0 + ? this.contextItem.metadata.disabledReasons.join(', ') : translate('DuoChatContextItemPopover.DisabledReason', 'This item is disabled'); }, iconName() { - switch (this.contextItem.type) { + switch (this.contextItem.category) { case 'merge_request': return 'merge-request'; case 'issue': @@ -89,7 +89,7 @@ export default { } }, itemTypeLabel() { - switch (this.contextItem.type) { + switch (this.contextItem.category) { case 'merge_request': return translate('DuoChatContextItemPopover.MergeRequest', 'Merge request'); case 'issue': @@ -109,7 +109,7 @@ export default { :target="target" triggers="hover focus" :placement="placement" - :title="contextItem.metadata.name" + :title="title" custom-class="gl-duo-chat-item-popover" >