diff --git a/app/static/css/custom.css b/app/static/css/custom.css index 7fa6712..2cfd07b 100644 --- a/app/static/css/custom.css +++ b/app/static/css/custom.css @@ -546,4 +546,60 @@ .app-link:hover { color: var(--highlight-color); +} + +/* Validation styles */ +.is-invalid { + border-color: #dc3545 !important; + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.is-valid { + border-color: #198754 !important; + padding-right: calc(1.5em + 0.75rem); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); +} + +.invalid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875em; + color: #dc3545; +} + +.invalid-feedback.d-block { + display: block !important; +} + +.valid-feedback { + display: none; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875em; + color: #198754; +} + +.valid-feedback.d-block { + display: block !important; +} + +/* Fix position for validation messages in table cells */ +td.position-relative { + position: relative; +} + +td .feedback { + position: absolute; + width: 100%; + left: 0; + top: 100%; + z-index: 5; } \ No newline at end of file diff --git a/app/static/js/validation.js b/app/static/js/validation.js index 9a60054..07a68a4 100644 --- a/app/static/js/validation.js +++ b/app/static/js/validation.js @@ -53,6 +53,13 @@ function validateAppName() { // Port Validation function validatePort(portField, protocolField) { const serverField = document.getElementById('server-id'); + // Create feedback element if it doesn't exist + if (!portField.nextElementSibling || !portField.nextElementSibling.classList.contains('feedback')) { + const feedback = document.createElement('div'); + feedback.className = 'feedback'; + portField.parentNode.insertBefore(feedback, portField.nextSibling); + } + const feedbackElement = portField.nextElementSibling; const submitButton = document.querySelector('button[type="submit"]'); @@ -65,6 +72,26 @@ function validatePort(portField, protocolField) { if (!port || !serverId) { clearFeedback(feedbackElement); + portField.classList.remove('is-invalid'); + return; + } + + // Check for duplicate ports within the form first + const allPortFields = document.querySelectorAll('input[name="port_numbers[]"]'); + const allProtocolFields = document.querySelectorAll('select[name="protocols[]"]'); + let duplicateFound = false; + + allPortFields.forEach((field, index) => { + if (field !== portField && + field.value === port && + allProtocolFields[index].value === protocol) { + duplicateFound = true; + } + }); + + if (duplicateFound) { + portField.classList.add('is-invalid'); + showError(feedbackElement, `Duplicate port ${port}/${protocol} in your form`); return; } @@ -75,22 +102,22 @@ function validatePort(portField, protocolField) { .then(response => response.json()) .then(data => { if (!data.valid) { + portField.classList.add('is-invalid'); showError(feedbackElement, data.message); if (data.edit_url) { - feedbackElement.innerHTML += ` Edit the conflicting app?`; + feedbackElement.innerHTML += ` Edit conflicting app`; } - portField.classList.add('is-invalid'); - submitButton.disabled = true; } else { - clearFeedback(feedbackElement); portField.classList.remove('is-invalid'); portField.classList.add('is-valid'); - submitButton.disabled = false; + showSuccess(feedbackElement, "Port available"); } }) .catch(error => { console.error('Validation error:', error); clearFeedback(feedbackElement); + portField.classList.remove('is-invalid'); + portField.classList.remove('is-valid'); }); }, 300); } diff --git a/app/templates/dashboard/app_form.html b/app/templates/dashboard/app_form.html index c00b72e..d5250f6 100644 --- a/app/templates/dashboard/app_form.html +++ b/app/templates/dashboard/app_form.html @@ -256,9 +256,10 @@ const tbody = document.querySelector('#ports-table tbody'); const newRow = document.createElement('tr'); newRow.innerHTML = ` - + +