// Application Name Validation function validateAppName() { const nameField = document.getElementById("app-name"); const serverField = document.getElementById("server-id"); const feedbackElement = document.getElementById("name-feedback"); const submitButton = document.querySelector('button[type="submit"]'); if (!nameField || !serverField) return; const name = nameField.value.trim(); const serverId = serverField.value; const appId = document.getElementById("app-id")?.value; if (!name || !serverId) { clearFeedback(feedbackElement); return; } // Debounce to avoid too many requests clearTimeout(nameField.timer); nameField.timer = setTimeout(() => { fetch( `/api/validate/app-name?name=${encodeURIComponent(name)}&server_id=${serverId}${appId ? "&app_id=" + appId : ""}`, ) .then((response) => response.json()) .then((data) => { if (!data.valid) { showError(feedbackElement, data.message); if (data.edit_url) { feedbackElement.innerHTML += ` Edit this app instead?`; } submitButton.disabled = true; } else if (data.warning) { showWarning(feedbackElement, data.warning); if (data.similar_apps && data.similar_apps.length > 0) { feedbackElement.innerHTML += '"; } submitButton.disabled = false; } else { showSuccess(feedbackElement, "App name is available"); submitButton.disabled = false; } }) .catch((error) => { console.error("Validation error:", error); clearFeedback(feedbackElement); }); }, 300); } // 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"]'); if (!portField || !serverField || !protocolField) return; const port = portField.value.trim(); const protocol = protocolField.value; const serverId = serverField.value; const appId = document.getElementById("app-id")?.value; 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"); clearFeedback(feedbackElement); showError( feedbackElement, `Duplicate port ${port}/${protocol} in your form`, ); return; } // Debounce to avoid too many requests clearTimeout(portField.timer); portField.timer = setTimeout(() => { fetch( `/api/validate/app-port?port_number=${encodeURIComponent(port)}&protocol=${protocol}&server_id=${serverId}${appId ? "&app_id=" + appId : ""}`, ) .then((response) => response.json()) .then((data) => { clearFeedback(feedbackElement); if (!data.valid) { portField.classList.add("is-invalid"); showError(feedbackElement, data.message); if (data.edit_url) { feedbackElement.innerHTML += ` Edit conflicting app`; } } else { portField.classList.remove("is-invalid"); portField.classList.add("is-valid"); showSuccess(feedbackElement, "Port available"); } }) .catch((error) => { console.error("Validation error:", error); clearFeedback(feedbackElement); portField.classList.remove("is-invalid"); portField.classList.remove("is-valid"); }); }, 300); } // Helper functions for feedback display function showError(element, message) { if (!element) return; element.className = "invalid-feedback d-block"; element.textContent = message; } function showWarning(element, message) { if (!element) return; element.className = "text-warning d-block"; element.textContent = message; } function showSuccess(element, message) { if (!element) return; element.className = "valid-feedback d-block"; element.textContent = message; } function clearFeedback(element) { if (!element) return; element.className = ""; element.textContent = ""; } // Set up event listeners when the document is loaded document.addEventListener("DOMContentLoaded", function () { const nameField = document.getElementById("app-name"); const serverField = document.getElementById("server-id"); if (nameField) { nameField.addEventListener("input", validateAppName); if (serverField) { serverField.addEventListener("change", validateAppName); } } // Set up validation for existing port fields setupPortValidation(); // Make sure new port rows get validation too const addPortButton = document.getElementById("add-port-btn"); if (addPortButton) { addPortButton.addEventListener("click", function () { // Wait a moment for the new row to be added setTimeout(setupPortValidation, 100); }); } }); function setupPortValidation() { const portFields = document.querySelectorAll('input[name="port_numbers[]"]'); const protocolFields = document.querySelectorAll( 'select[name="protocols[]"]', ); portFields.forEach((field, index) => { if (!field.hasValidationSetup) { const protocolField = protocolFields[index]; field.addEventListener("input", function () { validatePort(field, protocolField); }); if (protocolField) { protocolField.addEventListener("change", function () { validatePort(field, protocolField); }); } // Add a div for feedback if it doesn't exist if ( !field.nextElementSibling || !field.nextElementSibling.classList.contains("feedback") ) { const feedback = document.createElement("div"); feedback.className = "feedback"; field.parentNode.insertBefore(feedback, field.nextSibling); } field.hasValidationSetup = true; } }); }