diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py index 5d5c34e..276e9c2 100644 --- a/src/servala/core/crd.py +++ b/src/servala/core/crd.py @@ -327,6 +327,19 @@ class CrdModelFormMixin: field.widget = forms.HiddenInput() field.required = False + # Mark advanced fields with a CSS class and data attribute + advanced_fields = getattr(self, "ADVANCED_FIELDS", []) + for name, field in self.fields.items(): + if name in advanced_fields: + field.widget.attrs.update( + { + "class": ( + field.widget.attrs.get("class", "") + " advanced-field" + ).strip(), + "data-advanced": "true", + } + ) + if self.instance and self.instance.pk: self.fields["name"].disabled = True self.fields["name"].help_text = _("Name cannot be changed after creation.") @@ -513,7 +526,7 @@ class CrdModelFormMixin: pass -def generate_model_form_class(model): +def generate_model_form_class(model, advanced_fields=None): meta_attrs = { "model": model, "fields": "__all__", @@ -521,6 +534,7 @@ def generate_model_form_class(model): fields = { "Meta": type("Meta", (object,), meta_attrs), "__module__": "crd_models", + "ADVANCED_FIELDS": advanced_fields or [], } class_name = f"{model.__name__}ModelForm" return ModelFormMetaclass(class_name, (CrdModelFormMixin, ModelForm), fields) diff --git a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html index c9d947a..5857bdf 100644 --- a/src/servala/frontend/templates/includes/tabbed_fieldset_form.html +++ b/src/servala/frontend/templates/includes/tabbed_fieldset_form.html @@ -1,10 +1,23 @@ {% load i18n %} {% load get_field %} +{% load static %}
+ diff --git a/src/servala/static/css/servala.css b/src/servala/static/css/servala.css index eb4fd01..0ea8b28 100644 --- a/src/servala/static/css/servala.css +++ b/src/servala/static/css/servala.css @@ -302,3 +302,33 @@ html[data-bs-theme="dark"] .crd-form .nav-tabs .nav-link .mandatory-indicator { pointer-events: none; } } +.ml-auto { + margin-left: auto !important +} + +/* Advanced fields tab flash animation */ +@keyframes tab-pulse { + 0%, 100% { + background-color: transparent; + box-shadow: none; + } + 50% { + background-color: var(--brand-light); + box-shadow: 0 0 10px rgba(154, 99, 236, 0.3); + } +} + +html[data-bs-theme="dark"] @keyframes tab-pulse { + 0%, 100% { + background-color: transparent; + box-shadow: none; + } + 50% { + background-color: rgba(154, 99, 236, 0.2); + box-shadow: 0 0 10px rgba(154, 99, 236, 0.4); + } +} + +.nav-tabs .nav-link.tab-flash { + animation: tab-pulse 1s ease-in-out 2; +} diff --git a/src/servala/static/js/advanced-fields.js b/src/servala/static/js/advanced-fields.js new file mode 100644 index 0000000..989e61a --- /dev/null +++ b/src/servala/static/js/advanced-fields.js @@ -0,0 +1,83 @@ +/** + * Advanced Fields Toggle + * Handles showing/hiding advanced fields in CRD forms + */ +(function() { + 'use strict'; + + function flashTabsWithAdvancedFields() { + const advancedGroups = document.querySelectorAll('.advanced-field-group'); + const tabsToFlash = new Set(); + advancedGroups.forEach(function(group) { + const tabPane = group.closest('.tab-pane'); + if (tabPane) { + const tabId = tabPane.getAttribute('id'); + if (tabId) { + const tabButton = document.querySelector(`[data-bs-target="#${tabId}"]`); + if (tabButton && !tabButton.classList.contains('active')) { + tabsToFlash.add(tabButton); + } + } + } + }); + + tabsToFlash.forEach(function(tab) { + tab.classList.add('tab-flash'); + setTimeout(function() { + tab.classList.remove('tab-flash'); + }, 2000); + }); + } + + function initializeAdvancedFields() { + const advancedInputs = document.querySelectorAll('[data-advanced="true"]'); + + if (advancedInputs.length === 0) { + return; + } + + advancedInputs.forEach(function(input) { + const formGroup = input.closest('.form-group, .mb-3, .col-12, .col-md-6'); + if (formGroup) { + formGroup.classList.add('advanced-field-group', 'collapse'); + } + }); + + const toggleButton = document.getElementById('advanced-toggle'); + if (toggleButton) { + let isExpanded = false; + + document.querySelectorAll('.advanced-field-group').forEach(function(group) { + group.addEventListener('shown.bs.collapse', function() { + toggleButton.innerHTML = ' Hide Advanced Options'; + if (!isExpanded) { + isExpanded = true; + setTimeout(flashTabsWithAdvancedFields, 100); + } + }); + + group.addEventListener('hidden.bs.collapse', function() { + const anyVisible = Array.from(document.querySelectorAll('.advanced-field-group')).some( + g => g.classList.contains('show') + ); + if (!anyVisible) { + toggleButton.innerHTML = ' Show Advanced Options'; + isExpanded = false; + } + }); + }); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeAdvancedFields); + } else { + initializeAdvancedFields(); + } + + document.body.addEventListener('htmx:afterSwap', function(event) { + if (event.detail.target.id === 'service-form' || event.detail.target.closest('.crd-form')) { + setTimeout(initializeAdvancedFields, 100); + } + }); +})();