refactor: 🎨 auto save custom fields

This commit is contained in:
2025-12-27 22:34:18 +00:00
parent 8b001a047b
commit 7e1c4b126e
4 changed files with 179 additions and 8 deletions
+11 -2
View File
@@ -2906,8 +2906,17 @@ def register_routes(app, limiter=None):
new_values = {}
errors = []
# Handle both form data and JSON requests
if request.is_json:
submitted_data = request.json
else:
submitted_data = request.form
for field_key, field_def in field_defs.items():
submitted_value = request.form.get(f'custom_field_{field_key}', '')
if request.is_json:
submitted_value = submitted_data.get(f'custom_field_{field_key}', '')
else:
submitted_value = submitted_data.get(f'custom_field_{field_key}', '')
# Parse validation rules
validation_rules = field_def.get('validation_rules')
@@ -2944,7 +2953,7 @@ def register_routes(app, limiter=None):
conn.commit()
invalidate_cache_for_subnet(subnet_id)
if request.headers.get('Content-Type') == 'application/json':
if request.is_json or request.headers.get('Content-Type') == 'application/json':
return jsonify({'success': True, 'message': 'Custom fields updated successfully'})
return redirect(url_for('subnet', subnet_id=subnet_id))
+164
View File
@@ -0,0 +1,164 @@
// Auto-save custom fields on blur (subnet page)
document.addEventListener('DOMContentLoaded', () => {
const customFieldsForm = document.getElementById('custom-fields-form');
if (!customFieldsForm) {
return; // No custom fields form on this page
}
const subnetId = customFieldsForm.action.match(/\/subnet\/(\d+)\/update_custom_fields/)?.[1];
if (!subnetId) {
return;
}
// Get all form fields
const formFields = customFieldsForm.querySelectorAll('input, textarea, select');
const originalValues = new Map();
// Store original values
formFields.forEach(field => {
if (field.type === 'checkbox') {
originalValues.set(field, field.checked);
} else {
originalValues.set(field, field.value);
}
});
// Helper function to show toast notification (reuse from subnet.js if available)
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `fixed top-20 right-4 px-4 py-3 rounded-lg shadow-lg z-50 ${
type === 'success'
? 'bg-green-500 text-white'
: 'bg-red-500 text-white'
}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.transition = 'opacity 0.3s';
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Auto-resize textareas
const textareas = customFieldsForm.querySelectorAll('textarea');
textareas.forEach(textarea => {
textarea.style.overflow = 'hidden';
textarea.style.resize = 'none';
function autoResize() {
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
}
autoResize();
textarea.addEventListener('input', autoResize);
});
// Check if form has changes
function hasChanges() {
for (const field of formFields) {
let currentValue;
if (field.type === 'checkbox') {
currentValue = field.checked;
} else {
currentValue = field.value;
}
const originalValue = originalValues.get(field);
if (currentValue !== originalValue) {
return true;
}
}
return false;
}
// Save all custom fields
let saveInProgress = false;
async function saveCustomFields() {
if (saveInProgress) {
return; // Prevent multiple simultaneous saves
}
if (!hasChanges()) {
return; // No changes to save
}
saveInProgress = true;
// Show loading indicator on form
const originalOpacity = customFieldsForm.style.opacity;
customFieldsForm.style.opacity = '0.6';
customFieldsForm.style.pointerEvents = 'none';
try {
// Create FormData from form and convert to JSON
const formData = new FormData(customFieldsForm);
const data = {};
// Process all fields
for (const [key, value] of formData.entries()) {
data[key] = value;
}
// Handle checkboxes that weren't checked (they don't appear in FormData)
formFields.forEach(field => {
if (field.type === 'checkbox' && !field.checked) {
data[field.name] = '';
}
});
const response = await fetch(customFieldsForm.action, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
if (response.ok) {
// Update original values
formFields.forEach(field => {
if (field.type === 'checkbox') {
originalValues.set(field, field.checked);
} else {
originalValues.set(field, field.value);
}
});
showToast('Custom fields saved successfully', 'success');
} else {
const data = await response.json().catch(() => ({}));
const errorMsg = data.errors ? data.errors.join(', ') : (data.error || 'Failed to save custom fields');
showToast(errorMsg, 'error');
}
} catch (error) {
showToast('Error saving custom fields. Please try again.', 'error');
console.error('Error saving custom fields:', error);
} finally {
customFieldsForm.style.opacity = originalOpacity;
customFieldsForm.style.pointerEvents = '';
saveInProgress = false;
}
}
// Add blur event listeners to all fields
formFields.forEach(field => {
// Skip if it's a checkbox (we'll handle change event instead)
if (field.type === 'checkbox') {
field.addEventListener('change', () => {
// Small delay to ensure value is updated
setTimeout(saveCustomFields, 100);
});
} else {
field.addEventListener('blur', saveCustomFields);
}
});
// Prevent form submission (since we're using auto-save)
customFieldsForm.addEventListener('submit', (e) => {
e.preventDefault();
saveCustomFields();
});
});
+1
View File
@@ -0,0 +1 @@
document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("custom-fields-form");if(!e)return;let t=e.action.match(/\/subnet\/(\d+)\/update_custom_fields/)?.[1];if(!t)return;let r=e.querySelectorAll("input, textarea, select"),s=new Map;function o(e,t="success"){let r=document.createElement("div");r.className=`fixed top-20 right-4 px-4 py-3 rounded-lg shadow-lg z-50 ${"success"===t?"bg-green-500 text-white":"bg-red-500 text-white"}`,r.textContent=e,document.body.appendChild(r),setTimeout(()=>{r.style.transition="opacity 0.3s",r.style.opacity="0",setTimeout(()=>r.remove(),300)},3e3)}r.forEach(e=>{"checkbox"===e.type?s.set(e,e.checked):s.set(e,e.value)});let n=e.querySelectorAll("textarea");function c(){for(let e of r){let t;t="checkbox"===e.type?e.checked:e.value;let o=s.get(e);if(t!==o)return!0}return!1}n.forEach(e=>{function t(){e.style.height="auto",e.style.height=e.scrollHeight+"px"}e.style.overflow="hidden",e.style.resize="none",t(),e.addEventListener("input",t)});let l=!1;async function i(){if(l||!c())return;l=!0;let t=e.style.opacity;e.style.opacity="0.6",e.style.pointerEvents="none";try{let n=new FormData(e),i={};for(let[a,d]of n.entries())i[a]=d;r.forEach(e=>{"checkbox"!==e.type||e.checked||(i[e.name]="")});let u=await fetch(e.action,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});if(u.ok)r.forEach(e=>{"checkbox"===e.type?s.set(e,e.checked):s.set(e,e.value)}),o("Custom fields saved successfully","success");else{let y=await u.json().catch(()=>({})),f=y.errors?y.errors.join(", "):y.error||"Failed to save custom fields";o(f,"error")}}catch(h){o("Error saving custom fields. Please try again.","error"),console.error("Error saving custom fields:",h)}finally{e.style.opacity=t,e.style.pointerEvents="",l=!1}}r.forEach(e=>{"checkbox"===e.type?e.addEventListener("change",()=>{setTimeout(i,100)}):e.addEventListener("blur",i)}),e.addEventListener("submit",e=>{e.preventDefault(),i()})});
+3 -6
View File
@@ -149,12 +149,6 @@
</div>
{% endfor %}
</div>
<div class="flex justify-center mt-4">
<button type="submit" class="bg-gray-200 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 hover:cursor-pointer px-4 py-2 rounded-lg flex items-center gap-2">
<i class="fas fa-save"></i>
<span>Save</span>
</button>
</div>
</form>
{% else %}
<div class="space-y-4">
@@ -243,5 +237,8 @@
<script src="/static/js/export_csv.min.js"></script>
<script src="/static/js/subnet.min.js"></script>
<script src="/static/js/ip_history.min.js"></script>
{% if can_edit_subnet %}
<script src="/static/js/subnet_custom_fields.min.js"></script>
{% endif %}
</body>
</html>