From d562aa639a99ac7933cee964901b24daf7ed8857 Mon Sep 17 00:00:00 2001 From: lentschi Date: Sat, 18 Oct 2025 17:56:32 +0200 Subject: [PATCH] On #1007 AI code alternative --- app/assets/javascripts/application_legacy.js | 1 - app/assets/javascripts/group-order-form.js | 87 +++------- .../javascripts/unit-conversion-field.js | 20 +-- app/assets/javascripts/validation-utils.js | 155 ------------------ app/assets/stylesheets/application.scss | 1 - .../bootstrap_and_overrides.css.scss | 10 +- app/assets/stylesheets/validation.scss | 19 --- app/views/group_orders/_form.html.haml | 3 - 8 files changed, 27 insertions(+), 269 deletions(-) delete mode 100644 app/assets/javascripts/validation-utils.js delete mode 100644 app/assets/stylesheets/validation.scss diff --git a/app/assets/javascripts/application_legacy.js b/app/assets/javascripts/application_legacy.js index 18590f2a9..84138cd9b 100644 --- a/app/assets/javascripts/application_legacy.js +++ b/app/assets/javascripts/application_legacy.js @@ -18,7 +18,6 @@ //= require_self //= require big //= require units-converter -//= require validation-utils //= require migrate-units-form //= require article-form //= require unit-conversion-field diff --git a/app/assets/javascripts/group-order-form.js b/app/assets/javascripts/group-order-form.js index 81edda4d8..bbb9f2f2a 100644 --- a/app/assets/javascripts/group-order-form.js +++ b/app/assets/javascripts/group-order-form.js @@ -11,47 +11,14 @@ class GroupOrderForm { this.groupBalance = config.groupBalance; this.minimumBalance = config.minimumBalance; - ValidationUtils.setupCustomValidation(this.form$); - this.initializeIncreaseDecreaseButtons(); this.submitButton$.removeAttr('disabled'); - this.initializeFormValidation(); } initializeIncreaseDecreaseButtons() { this.articleRows$.each((_, element) => this.initializeOrderArticleRow($(element))); } - initializeFormValidation() { - this.form$.on('submit', (e) => { - // Update all validation messages first - this.articleRows$.each((_, row) => { - const row$ = $(row); - this.updateValidationMessages(row$); - }); - - // Check if form is valid - browser will prevent submission if invalid - if (!this.form$[0].checkValidity()) { - e.preventDefault(); - - // Show error messages for all invalid inputs - this.form$.find(':invalid').each((_, invalidInput) => { - const input$ = $(invalidInput); - if (input$.hasClass('goa-quantity')) { - // Trigger validation to show error message - ValidationUtils.validateNumericInput(input$); - } - }); - - // Focus on first invalid input - const firstInvalid = this.form$.find(':invalid').first(); - if (firstInvalid.length > 0) { - firstInvalid.focus(); - } - } - }); - } - initializeOrderArticleRow(row$) { const quantity$ = row$.find('.goa-quantity'); const tolerance$ = row$.find('.goa-tolerance'); @@ -67,20 +34,29 @@ class GroupOrderForm { row$.find('.btn-ordering.increase').click((event) => this.increaseOrDecrease($(event.target).parents('.btn-group').find('input.numeric'), true)); quantityAndTolerance$.change(() => { + this.updateValidationErrors(quantity$); this.updateMissingUnits(row$, quantity$); this.updateBalance(); - this.updateValidationMessages(row$); }); quantityAndTolerance$.keyup(() => quantity$.trigger('change')); + } - // Handle validation events for quantity field only - quantity$.on('invalid', (e) => { - e.preventDefault(); - e.stopPropagation(); - this.updateValidationMessages(row$); - }); + updateValidationErrors(valueField$) { + const errorContainer$ = valueField$ + // TODO: This parent().parent() is quite arbitrary: + .parent().parent(); + errorContainer$.find('.quantity-field-invalid').remove(); - this.updateValidationMessages(row$); + if (valueField$.is(':invalid')) { + if (valueField$[0].validity.stepMismatch || valueField$[0].validity.rangeUnderflow) { + const errorSpan$ = $(`
${I18n.t('errors.step_error', {min: 0, granularity: valueField$.attr('step')})}
`); + errorContainer$.append(errorSpan$); + } + if (valueField$[0].validity.rangeOverflow) { + const errorSpan$ = $(`
${I18n.t('errors.maximum_quantity_error', {max: valueField$.attr('max')})}
`); + errorContainer$.append(errorSpan$); + } + } } updateBalance() { @@ -102,8 +78,8 @@ class GroupOrderForm { // determine bgcolor and submit button state according to balance var bgcolor = ''; if (balance < this.minimumBalance) { - bgcolor = '#FF0000'; - this.submitButton$.attr('disabled', 'disabled') + bgcolor = '#FF0000'; + this.submitButton$.attr('disabled', 'disabled') } else { this.submitButton$.removeAttr('disabled') } @@ -269,30 +245,5 @@ class GroupOrderForm { var remainder = Big(quantity).mod(packSize).toNumber(); return (remainder > 0 && (Big(remainder).add(tolerance).toNumber() >= packSize)); } - - updateValidationMessages(row$) { - const quantity$ = row$.find('.goa-quantity'); - - if (quantity$.length === 0) return; - - const inputElement = quantity$[0]; - - // Clear any existing validation state - inputElement.setCustomValidity(''); - - // Use ValidationUtils for validation with error spans - ValidationUtils.validateNumericInput(quantity$); - - // Special case: item not available (max = 0) - const rawValue = quantity$.val().trim(); - const inputValue = parseFloat(rawValue); - const maxValue = parseFloat(quantity$.attr('max')); - - if (maxValue === 0 && inputValue > 0) { - const message = I18n.t('errors.item_not_available'); - inputElement.setCustomValidity(message); - ValidationUtils.showValidationMessage(quantity$, message); - } - } } diff --git a/app/assets/javascripts/unit-conversion-field.js b/app/assets/javascripts/unit-conversion-field.js index a5773f076..08226d7d5 100644 --- a/app/assets/javascripts/unit-conversion-field.js +++ b/app/assets/javascripts/unit-conversion-field.js @@ -185,22 +185,14 @@ const unitLabel = this.unitSelectOptions.find(option => option.value === unit).label; this.conversionResult$.text('= ' + this.getConversionResult() + ' x ' + unitLabel); this.conversionResult$.parent().find('.numeric-step-error').remove(); + if (this.quantityInput$.is(':invalid')) { + this.applyButton$.attr('disabled', 'disabled'); - const errorSpan$ = $('
'); - - // Use ValidationUtils for proper custom error messages - const isValid = ValidationUtils.validateNumericInput(this.quantityInput$, { - errorSpan$: errorSpan$ - }); - - if (isValid) { - this.applyButton$.removeAttr('disabled'); + const errorSpan$ = $(`
${I18n.t('errors.step_error', {min: 0, granularity: this.quantityInput$.attr('step')})}
`); + errorSpan$.show(); + this.conversionResult$.after(errorSpan$); } else { - this.applyButton$.attr('disabled', 'disabled'); - if (errorSpan$.text()) { - errorSpan$.show(); - this.conversionResult$.after(errorSpan$); - } + this.applyButton$.removeAttr('disabled'); } } diff --git a/app/assets/javascripts/validation-utils.js b/app/assets/javascripts/validation-utils.js deleted file mode 100644 index 59cadcd08..000000000 --- a/app/assets/javascripts/validation-utils.js +++ /dev/null @@ -1,155 +0,0 @@ -// Shared validation utilities for form inputs -class ValidationUtils { - /** - * Sets up custom validation messages while keeping browser validation active - * @param {jQuery} form$ - The form element - */ - static setupCustomValidation(form$) { - if (form$.length === 0) return; - - // Keep HTML5 validation active but override messages - // Don't set novalidate - we want the browser to prevent submission - - // Set up custom validation for quantity inputs only - form$.find('input[type="number"].goa-quantity').each((_, input) => { - const input$ = $(input); - - // Set up event handlers for custom validation messages - input$.on('invalid.customValidation', (e) => { - e.preventDefault(); // Prevent browser validation popup - - // Apply our custom validation and show message in popover - ValidationUtils.validateNumericInput(input$, { - showInSpanOnly: true // Don't use setCustomValidity for display - }); - }); - }); - } - /** - * Validates a numeric input and sets custom validation messages - * @param {jQuery} input$ - The input element to validate - * @param {Object} options - Validation options - * @returns {boolean} - Whether the input is valid - */ - static validateNumericInput(input$, options = {}) { - if (input$.length === 0) return true; - - const inputElement = input$[0]; - const rawValue = input$.val().trim().replace(',', '.'); - const inputValue = parseFloat(rawValue); - - let customMessage = ''; - - // Only validate if we have a valid number - if (rawValue !== '' && !isNaN(inputValue)) { - // Quantity field validation only - customMessage = ValidationUtils.validateQuantityField(input$, inputValue); - } - - // Show validation message in error span (avoid popover conflicts) - ValidationUtils.showValidationMessage(input$, customMessage); - - // Update visual error span if it exists (keep for backward compatibility) - if (options.errorSpan$ && options.errorSpan$.length > 0) { - if (customMessage) { - options.errorSpan$.text(customMessage).show(); - } else { - options.errorSpan$.hide(); - } - } - - // Set custom validation message for browser validation - // This will be used by the browser to prevent form submission - if (!options.showInSpanOnly) { - inputElement.setCustomValidity(customMessage); - } - - // Return validation state - return customMessage === ''; - } - - /** - * Validates a quantity field - * @param {jQuery} input$ - The quantity input element - * @param {number} inputValue - The parsed input value - * @returns {string} - Error message or empty string if valid - */ - static validateQuantityField(input$, inputValue) { - // Get validation constraints from HTML attributes - const maxValue = input$.attr('max') ? parseFloat(input$.attr('max')) : null; - const minValue = parseFloat(input$.attr('min')) || 0; - const stepValue = parseFloat(input$.attr('step')) || 1; - - // Check maximum quantity first (highest priority) - if (maxValue !== null && inputValue > maxValue) { - return I18n.t('errors.maximum_quantity_error', { max: maxValue }); - } - // Check minimum value - else if (inputValue < minValue) { - return I18n.t('errors.step_error', { min: minValue, granularity: stepValue }); - } - // Check step/granularity (allow for floating point precision issues) - else if (stepValue > 0) { - const remainder = ((inputValue - minValue) % stepValue); - if (Math.abs(remainder) > 0.0001 && Math.abs(remainder - stepValue) > 0.0001) { - return I18n.t('errors.step_error', { min: minValue, granularity: stepValue }); - } - } - - return ''; - } - - /** - * Shows or hides validation message using error spans (avoids popover conflicts) - * @param {jQuery} input$ - The input element - * @param {string} message - The validation message (empty to hide) - */ - static showValidationMessage(input$, message) { - if (input$.length === 0) return; - - // Find the error span for this input - const errorSpan$ = input$.closest('.group-order-input').find('.numeric-step-error'); - - if (message) { - // Show error message in span and add error styling - if (errorSpan$.length > 0) { - errorSpan$.text(message).show(); - } - input$.addClass('validation-error'); - } else { - // Hide error message and remove styling - if (errorSpan$.length > 0) { - errorSpan$.hide(); - } - input$.removeClass('validation-error'); - } - } - - /** - * Hides all validation messages - */ - static hideAllValidationMessages() { - $('.validation-error').removeClass('validation-error'); - $('.numeric-step-error').hide(); - } - - - - /** - * Sets up validation for a form input - * @param {jQuery} input$ - The input element - * @param {Object} options - Validation options - */ - static setupInputValidation(input$, options = {}) { - if (input$.length === 0) return; - - // Only set up event handlers, don't trigger validation immediately - // to avoid recursion when called from existing event handlers - input$.on('invalid.validationUtils', (e) => { - e.preventDefault(); - e.stopPropagation(); - ValidationUtils.validateNumericInput(input$, options); - return false; - }); - } -} \ No newline at end of file diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 5b7785d3d..dbd57ebbf 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -11,4 +11,3 @@ @import "list.missing"; @import "recurring_select"; @import "actiontext"; -@import "validation"; diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.scss b/app/assets/stylesheets/bootstrap_and_overrides.css.scss index cee179f3e..299f2076b 100644 --- a/app/assets/stylesheets/bootstrap_and_overrides.css.scss +++ b/app/assets/stylesheets/bootstrap_and_overrides.css.scss @@ -314,7 +314,7 @@ tr.order-article { } .units_received_cell { - + .btn-group .btn-ordering { margin-top: 0; @@ -704,8 +704,7 @@ td.mr-1 > *:last-child { user-select: none; } -.numeric-step-error { - display: none; +.quantity-field-invalid { color: #b94a48; } @@ -713,11 +712,6 @@ td.mr-1 > *:last-child { border: 1px solid #b94a48; } -.btn-group.numeric-step:has(> input:invalid) + .numeric-step-error { - display: block; -} - - .form-group .select2-container { width: 200px; } diff --git a/app/assets/stylesheets/validation.scss b/app/assets/stylesheets/validation.scss deleted file mode 100644 index b44d35826..000000000 --- a/app/assets/stylesheets/validation.scss +++ /dev/null @@ -1,19 +0,0 @@ -// Simple validation error styling for inputs -input.validation-error { - border-color: #d9534f !important; - box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px #ce8483 !important; - - &:focus { - border-color: #d9534f !important; - box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px #ce8483 !important; - } -} - -// Style error message spans -.numeric-step-error { - color: #d9534f; - font-size: 12px; - font-weight: 500; - margin-top: 2px; - display: block; -} \ No newline at end of file diff --git a/app/views/group_orders/_form.html.haml b/app/views/group_orders/_form.html.haml index 698f19d6d..f1fd4c43d 100644 --- a/app/views/group_orders/_form.html.haml +++ b/app/views/group_orders/_form.html.haml @@ -116,7 +116,6 @@ - quantity_data['used_quantity'] = @ordering_data[:order_articles][order_article.id][:used_quantity] - quantity_data['price'] = @ordering_data[:order_articles][order_article.id][:price] - quantity_data['minimum_order_quantity'] = @ordering_data[:order_articles][order_article.id][:minimum_order_quantity] unless @ordering_data[:order_articles][order_article.id][:minimum_order_quantity].nil? - - quantity_data['maximum_order_quantity'] = @ordering_data[:order_articles][order_article.id][:maximum_order_quantity] unless @ordering_data[:order_articles][order_article.id][:maximum_order_quantity].nil? - quantity_data['e2e-order-article-id'] = order_article.id %td.used-unused %span.used= number_with_precision(@ordering_data[:order_articles][order_article.id][:used_quantity], precision: 3, strip_insignificant_zeros: true) @@ -129,8 +128,6 @@ %a.btn.btn-ordering.increase %i.glyphicon.glyphicon-plus %input.goa-quantity{type: "number", name: "group_order[group_order_articles_attributes][#{order_article.id}][quantity]", value: @ordering_data[:order_articles][order_article.id][:quantity], data: quantity_data, autocomplete: 'off', class: 'form-control numeric', style: ('display:none' if @order.stockit?), min: 0, max: (@ordering_data[:order_articles][order_article.id][:quantity_available]), step: order_article.article_version.group_order_granularity} - %span.numeric-step-error{style: 'display: none;'} - = t('errors.step_error', granularity: order_article.article_version.group_order_granularity, min: 0) %td.used-unused-tolerance{style: ('display:none' if @order.stockit?)} - if (order_article.article_version.uses_tolerance?)