diff --git a/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_item_consumer_modal.vue b/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_item_consumer_modal.vue
index bab678ccff11259ebd802072ee780ae4b4f9e1a2..22c1d43f68acf1bbe90562e6ef8d5a25201d73c7 100644
--- a/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_item_consumer_modal.vue
+++ b/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_item_consumer_modal.vue
@@ -26,7 +26,7 @@ export default {
data() {
return {
isOpen: true,
- targetId: this.item.project?.id || null,
+ targetId: null,
error: null,
};
},
@@ -47,6 +47,9 @@ export default {
itemType: this.itemTypeLabel,
});
},
+ isProjectValid() {
+ return Boolean(this.targetId);
+ },
isPrivateItem() {
return !this.item.public;
},
@@ -132,8 +135,15 @@ export default {
:label="s__('AICatalog|Project')"
:label-description="projectLabelDescription"
label-for="target-id"
+ :state="isProjectValid"
+ :invalid-feedback="__('Project is required.')"
>
-
+
diff --git a/ee/app/assets/javascripts/ai/catalog/components/form_project_dropdown.vue b/ee/app/assets/javascripts/ai/catalog/components/form_project_dropdown.vue
index 3a4bdb7836bbe955510c18e9b4d949c4cf8a137d..2db47205e187d7b69850656b5637b594db9996ee 100644
--- a/ee/app/assets/javascripts/ai/catalog/components/form_project_dropdown.vue
+++ b/ee/app/assets/javascripts/ai/catalog/components/form_project_dropdown.vue
@@ -23,6 +23,11 @@ export default {
required: false,
default: null,
},
+ isValid: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
value: {
type: String,
required: false,
@@ -147,6 +152,7 @@ export default {
:items="projectList"
:toggle-id="id"
:toggle-text="projectDropdownText"
+ :toggle-class="{ 'gl-shadow-inner-1-red-500': !isValid }"
:header-text="__('Select a project')"
:loading="isLoadingInitial"
searchable
diff --git a/ee/spec/frontend/ai/catalog/components/ai_catalog_item_consumer_modal_spec.js b/ee/spec/frontend/ai/catalog/components/ai_catalog_item_consumer_modal_spec.js
index f84116cf34578772240c46ba6d527a988b69a18f..a7fa9f6f3b0b898de89621d8136d2bee0a9f4c50 100644
--- a/ee/spec/frontend/ai/catalog/components/ai_catalog_item_consumer_modal_spec.js
+++ b/ee/spec/frontend/ai/catalog/components/ai_catalog_item_consumer_modal_spec.js
@@ -64,8 +64,8 @@ describe('AiCatalogItemConsumerModal', () => {
expect(findProjectDropdown().exists()).toBe(true);
});
- it('renders project dropdown with preselected project', () => {
- expect(findProjectDropdown().props('value')).toBe(agent.project.id);
+ it('renders project dropdown with no preselected project', () => {
+ expect(findProjectDropdown().props('value')).toBe(null);
});
it('renders alert when there was a problem fetching the projects', async () => {
@@ -82,6 +82,44 @@ describe('AiCatalogItemConsumerModal', () => {
});
});
+ describe('isProjectValid computed property', () => {
+ it('returns false when targetId is null', () => {
+ expect(wrapper.vm.isProjectValid).toBe(false);
+ });
+
+ it('returns true when targetId has a value', async () => {
+ const projectId = 'gid://gitlab/Project/1000000';
+ await findProjectDropdown().vm.$emit('input', projectId);
+
+ expect(wrapper.vm.isProjectValid).toBe(true);
+ });
+ });
+
+ describe('GlFormGroup validation state', () => {
+ it('passes invalid state to form group when no project is selected', () => {
+ expect(findFormGroup().props('state')).toBe(false);
+ expect(findFormGroup().props('invalidFeedback')).toBe('Project is required.');
+ });
+
+ it('passes valid state to form group when project is selected', async () => {
+ const projectId = 'gid://gitlab/Project/1000000';
+ await findProjectDropdown().vm.$emit('input', projectId);
+
+ expect(findFormGroup().props('state')).toBe(true);
+ });
+
+ it('passes isValid prop to project dropdown', () => {
+ expect(findProjectDropdown().props('isValid')).toBe(false);
+ });
+
+ it('passes isValid prop as true to project dropdown when project is selected', async () => {
+ const projectId = 'gid://gitlab/Project/1000000';
+ await findProjectDropdown().vm.$emit('input', projectId);
+
+ expect(findProjectDropdown().props('isValid')).toBe(true);
+ });
+ });
+
describe('when submitting the form', () => {
it('emits the submit event', async () => {
const projectId = 'gid://gitlab/Project/1000000';
diff --git a/ee/spec/frontend/ai/catalog/components/form_project_dropdown_spec.js b/ee/spec/frontend/ai/catalog/components/form_project_dropdown_spec.js
index 9cf81a71c35c51758cf44fd2d88be5090fedd546..e7ecbdd0b5655384c025e31c53b52747a651270e 100644
--- a/ee/spec/frontend/ai/catalog/components/form_project_dropdown_spec.js
+++ b/ee/spec/frontend/ai/catalog/components/form_project_dropdown_spec.js
@@ -201,4 +201,26 @@ describe('FormProjectDropdown', () => {
expect(wrapper.emitted('input')).toEqual([['gid://gitlab/Project/1']]);
});
});
+
+ describe('isValid prop', () => {
+ it('defaults to true when not provided', () => {
+ expect(findListbox().props('toggleClass')).toEqual({});
+ });
+
+ it('applies error styling when isValid is false', () => {
+ createComponent({ props: { isValid: false } });
+
+ expect(findListbox().props('toggleClass')).toEqual({
+ 'gl-shadow-inner-1-red-500': true,
+ });
+ });
+
+ it('does not apply error styling when isValid is true', () => {
+ createComponent({ props: { isValid: true } });
+
+ expect(findListbox().props('toggleClass')).toEqual({
+ 'gl-shadow-inner-1-red-500': false,
+ });
+ });
+ });
});
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index caff951fe6ea120bd272f0be7cd30e16a6238ed5..2a501dabf4615aca55634c468333f7f6c498d6c3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -50421,6 +50421,9 @@ msgstr ""
msgid "Project information"
msgstr ""
+msgid "Project is required."
+msgstr ""
+
msgid "Project members"
msgstr ""