From 13e6403babf21f00e41bd436a004bbb501cb008b Mon Sep 17 00:00:00 2001 From: Lina Fowler Date: Thu, 24 Jul 2025 14:42:29 -0600 Subject: [PATCH 1/6] Updated validation message to contain group and project field names --- .../new/components/group_project_fields.vue | 209 +++++++++++++++++- 1 file changed, 206 insertions(+), 3 deletions(-) diff --git a/ee/app/assets/javascripts/registrations/groups/new/components/group_project_fields.vue b/ee/app/assets/javascripts/registrations/groups/new/components/group_project_fields.vue index 81fb8cb4060a7a..97696f47fc56e6 100644 --- a/ee/app/assets/javascripts/registrations/groups/new/components/group_project_fields.vue +++ b/ee/app/assets/javascripts/registrations/groups/new/components/group_project_fields.vue @@ -25,6 +25,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + expose: ['validateForm'], props: { importGroup: { type: Boolean, @@ -69,6 +70,13 @@ export default { currentApiRequestController: null, groupPathWithoutSuggestion: null, selectedTemplateName: this.templateName, + // Track current values for validation + currentProjectName: this.projectName, + // Validation state for accessibility + validation: { + groupName: { isValid: true, message: '' }, + projectName: { isValid: true, message: '' }, + }, }; }, computed: { @@ -90,8 +98,49 @@ export default { } if (this.projectName) { + this.currentProjectName = this.projectName; this.onProjectUpdate(this.projectName); } + + // Add form submission event listeners + this.$nextTick(() => { + this.createForm = document.querySelector('.js-groups-projects-form'); + this.importForm = document.querySelector('.js-import-project-form'); + + if (this.createForm) { + this.createForm.addEventListener('submit', this.handleFormSubmit.bind(this)); + } + + if (this.importForm) { + this.importForm.addEventListener('submit', this.handleImportFormSubmit.bind(this)); + } + + // Add tab switch event listeners to reset validation + this.addTabSwitchListeners(); + }); + }, + + beforeDestroy() { + // Clean up event listeners + if (this.createForm) { + this.createForm.removeEventListener('submit', this.handleFormSubmit); + } + + if (this.importForm) { + this.importForm.removeEventListener('submit', this.handleImportFormSubmit); + } + + // Clean up tab switch listeners + const createTab = document.querySelector('#blank-project-tab'); + const importTab = document.querySelector('#import-project-tab'); + + if (createTab) { + createTab.removeEventListener('click', this.resetValidation); + } + + if (importTab) { + importTab.removeEventListener('click', this.resetValidation); + } }, methods: { ...mapActions(['setStoreGroupName', 'setStoreGroupPath']), @@ -135,7 +184,78 @@ export default { debouncedOnGroupUpdate: debounce(function debouncedUpdate(slug) { this.setSuggestedSlug(slug); }, DEBOUNCE_TIMEOUT_DURATION), + + // Validation methods for accessibility + validateGroupName(value) { + if (!value || !value.trim()) { + this.validation.groupName = { + isValid: false, + message: __('Group name is required'), + }; + return false; + } + + this.validation.groupName = { isValid: true, message: '' }; + return true; + }, + + validateProjectName(value) { + if (!value || !value.trim()) { + this.validation.projectName = { + isValid: false, + message: __('Project name is required'), + }; + return false; + } + + this.validation.projectName = { isValid: true, message: '' }; + return true; + }, + + validateForm() { + let isValid = true; + + // Validate group name if needed + if (!this.groupPersisted || this.importGroup) { + const groupNameValue = this.storeGroupName || ''; + if (!this.validateGroupName(groupNameValue)) { + isValid = false; + } + } + + // Validate project name if needed + if (!this.importGroup) { + const projectNameValue = this.currentProjectName || ''; + if (!this.validateProjectName(projectNameValue)) { + isValid = false; + } + } + + // Hide server-side errors when client-side validation is handling it + if (!isValid) { + const errorExplanation = document.getElementById('error_explanation'); + if (errorExplanation) { + errorExplanation.style.display = 'none'; + } + + // Focus first invalid field + this.$nextTick(() => { + const firstError = this.$el.querySelector('[aria-invalid="true"]'); + if (firstError) { + firstError.focus(); + } + }); + } + + return isValid; + }, + onGroupUpdate(value) { + // Clear validation error when user starts typing + if (value && value.trim()) { + this.validation.groupName = { isValid: true, message: '' }; + } + const slug = slugify(value); this.setStoreGroupName(value); @@ -144,12 +264,66 @@ export default { this.setStoreGroupPath(slug); return this.debouncedOnGroupUpdate(slug); }, + onProjectUpdate(value) { + // Store the current value for validation + this.currentProjectName = value; + + // Clear validation error when user starts typing + if (value && value.trim()) { + this.validation.projectName = { isValid: true, message: '' }; + } + this.projectPath = slugify(convertUnicodeToAscii(value)) || DEFAULT_PROJECT_PATH; }, + selectTemplate(value) { this.selectedTemplateName = value; }, + + handleFormSubmit(event) { + const isValid = this.validateForm(); + + if (!isValid) { + event.preventDefault(); + event.stopPropagation(); + return false; + } + + return true; + }, + + handleImportFormSubmit(event) { + const isValid = this.validateForm(); + + if (!isValid) { + event.preventDefault(); + event.stopPropagation(); + return false; + } + + return true; + }, + + addTabSwitchListeners() { + const createTab = document.querySelector('#blank-project-tab'); + const importTab = document.querySelector('#import-project-tab'); + + if (createTab) { + createTab.addEventListener('click', this.resetValidation.bind(this)); + } + + if (importTab) { + importTab.addEventListener('click', this.resetValidation.bind(this)); + } + }, + + resetValidation() { + this.validation = { + groupName: { isValid: true, message: '' }, + projectName: { isValid: true, message: '' }, + }; + }, }, i18n: { groupNameLabel: s__('ProjectsNew|Group name'), @@ -163,6 +337,7 @@ export default { }, }; +