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 ""