From 8acff712b2bdc683c38494b4f643aa159fa1b2c6 Mon Sep 17 00:00:00 2001 From: Florie Guibert Date: Mon, 15 Dec 2025 12:24:48 +1000 Subject: [PATCH] AI catalog list: Display Unlisted badge for soft deleted items Behind :global_ai_catalog feature flag --- .../components/ai_catalog_list_item.vue | 49 ++++++++++++++++++- .../ai_catalog_item.fragment.graphql | 1 + .../components/ai_catalog_list_item_spec.js | 31 ++++++++++++ ee/spec/frontend/ai/catalog/mock_data.js | 2 + locale/gitlab.pot | 6 +++ 5 files changed, 87 insertions(+), 2 deletions(-) diff --git a/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_list_item.vue b/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_list_item.vue index ce84b23ea08c52..527e38a3dac64c 100644 --- a/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_list_item.vue +++ b/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_list_item.vue @@ -8,7 +8,7 @@ import { GlIcon, GlTooltipDirective, } from '@gitlab/ui'; -import { __, s__ } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { VISIBILITY_TYPE_ICON, @@ -17,7 +17,13 @@ import { VISIBILITY_LEVEL_PRIVATE_STRING, } from '~/visibility_level/constants'; import FoundationalIcon from 'ee/ai/components/foundational_icon.vue'; -import { AI_CATALOG_TYPE_THIRD_PARTY_FLOW } from '../constants'; +import { + AI_CATALOG_TYPE_THIRD_PARTY_FLOW, + AI_CATALOG_ITEM_LABELS, + AI_CATALOG_CONSUMER_TYPE_PROJECT, + AI_CATALOG_CONSUMER_TYPE_GROUP, + AI_CATALOG_CONSUMER_LABELS, +} from '../constants'; export default { name: 'AiCatalogListItem', @@ -33,6 +39,11 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + inject: { + projectId: { + default: null, + }, + }, props: { item: { type: Object, @@ -44,6 +55,20 @@ export default { }, }, computed: { + isProjectNamespace() { + return Boolean(this.projectId); + }, + itemTypeLabel() { + return AI_CATALOG_ITEM_LABELS[this.item.itemType]; + }, + targetType() { + return this.isProjectNamespace + ? AI_CATALOG_CONSUMER_TYPE_PROJECT + : AI_CATALOG_CONSUMER_TYPE_GROUP; + }, + targetTypeLabel() { + return AI_CATALOG_CONSUMER_LABELS[this.targetType]; + }, actionItems() { return this.itemTypeConfig.actionItems?.(this.item) || []; }, @@ -96,6 +121,17 @@ export default { isThirdPartyFlow() { return this.item.itemType === AI_CATALOG_TYPE_THIRD_PARTY_FLOW; }, + softDeletedTooltipText() { + return sprintf( + s__( + 'AICatalog|This %{itemType} was removed from the AI Catalog. You can still use it in this %{targetType}.', + ), + { + itemType: this.itemTypeLabel, + targetType: this.targetTypeLabel, + }, + ); + }, }, }; @@ -176,6 +212,15 @@ export default { {{ s__('AICatalog|Update available') }} +
+ {{ s__('AICatalog|Unlisted') }} +
{ const findDisclosureDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem); const findFoundationalIcon = () => wrapper.findComponent(FoundationalIcon); const findUpdateAvailableLabel = () => wrapper.findByTestId('ai-catalog-item-update'); + const findUpdateUnlistedBadge = () => wrapper.findByTestId('ai-catalog-item-unlisted'); beforeEach(() => { createComponent(); @@ -334,4 +335,34 @@ describe('AiCatalogListItem', () => { }); }); }); + + describe('unlisted badge', () => { + describe('when item is soft deleted', () => { + beforeEach(() => { + createComponent({ + item: { ...mockItem, softDeleted: true }, + }); + }); + it('renders Unlisted badge when softDeleted is true', () => { + expect(findUpdateUnlistedBadge().findComponent(GlBadge).text()).toBe('Unlisted'); + }); + + it('renders tooltip with correct text', () => { + expect(findUpdateUnlistedBadge().attributes('title')).toBe( + 'This agent was removed from the AI Catalog. You can still use it in this group.', + ); + }); + }); + + describe('when item is not soft deleted', () => { + beforeEach(() => { + createComponent({ + item: { ...mockItem, isUpdateAvailable: false }, + }); + }); + it('does not render Unlisted badge when softDeleted is false', () => { + expect(findUpdateUnlistedBadge().exists()).toBe(false); + }); + }); + }); }); diff --git a/ee/spec/frontend/ai/catalog/mock_data.js b/ee/spec/frontend/ai/catalog/mock_data.js index 1b1c439c4cf043..0c49947d3e3381 100644 --- a/ee/spec/frontend/ai/catalog/mock_data.js +++ b/ee/spec/frontend/ai/catalog/mock_data.js @@ -175,6 +175,7 @@ const mockAgentFactory = (overrides = {}) => ({ itemType: 'AGENT', description: 'A helpful AI assistant for testing purposes', createdAt: '2024-01-15T10:30:00Z', + softDeleted: false, public: true, updatedAt: '2024-08-21T14:30:00Z', latestVersion: mockBaseVersion, @@ -468,6 +469,7 @@ const mockFlowFactory = (overrides = {}) => ({ createdAt: '2024-01-15T10:30:00Z', public: true, updatedAt: '2024-08-21T14:30:00Z', + softDeleted: false, foundational: false, latestVersion: mockBaseVersion, userPermissions: mockUserPermissions, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ae50aa33f5dc37..8006b79559e121 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3073,6 +3073,9 @@ msgstr "" msgid "AICatalog|This %{itemType} requires approval from your parent group owner before it can be used" msgstr "" +msgid "AICatalog|This %{itemType} was removed from the AI Catalog. You can still use it in this %{targetType}." +msgstr "" + msgid "AICatalog|This YAML configuration file determines the prompts, tools, and capabilities of your flow. Required properties: injectGatewayToken, image, commands" msgstr "" @@ -3115,6 +3118,9 @@ msgstr "" msgid "AICatalog|Type" msgstr "" +msgid "AICatalog|Unlisted" +msgstr "" + msgid "AICatalog|Update available" msgstr "" -- GitLab