October feature list #226
4 changed files with 142 additions and 1 deletions
|
|
@ -327,6 +327,19 @@ class CrdModelFormMixin:
|
||||||
field.widget = forms.HiddenInput()
|
field.widget = forms.HiddenInput()
|
||||||
field.required = False
|
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:
|
if self.instance and self.instance.pk:
|
||||||
self.fields["name"].disabled = True
|
self.fields["name"].disabled = True
|
||||||
self.fields["name"].help_text = _("Name cannot be changed after creation.")
|
self.fields["name"].help_text = _("Name cannot be changed after creation.")
|
||||||
|
|
@ -513,7 +526,7 @@ class CrdModelFormMixin:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def generate_model_form_class(model):
|
def generate_model_form_class(model, advanced_fields=None):
|
||||||
meta_attrs = {
|
meta_attrs = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"fields": "__all__",
|
"fields": "__all__",
|
||||||
|
|
@ -521,6 +534,7 @@ def generate_model_form_class(model):
|
||||||
fields = {
|
fields = {
|
||||||
"Meta": type("Meta", (object,), meta_attrs),
|
"Meta": type("Meta", (object,), meta_attrs),
|
||||||
"__module__": "crd_models",
|
"__module__": "crd_models",
|
||||||
|
"ADVANCED_FIELDS": advanced_fields or [],
|
||||||
}
|
}
|
||||||
class_name = f"{model.__name__}ModelForm"
|
class_name = f"{model.__name__}ModelForm"
|
||||||
return ModelFormMetaclass(class_name, (CrdModelFormMixin, ModelForm), fields)
|
return ModelFormMetaclass(class_name, (CrdModelFormMixin, ModelForm), fields)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,23 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load get_field %}
|
{% load get_field %}
|
||||||
|
{% load static %}
|
||||||
<form class="form form-vertical crd-form"
|
<form class="form form-vertical crd-form"
|
||||||
method="post"
|
method="post"
|
||||||
{% if form_action %}action="{{ form_action }}"{% endif %}>
|
{% if form_action %}action="{{ form_action }}"{% endif %}>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include "frontend/forms/errors.html" %}
|
{% include "frontend/forms/errors.html" %}
|
||||||
|
{% if form.ADVANCED_FIELDS %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-secondary ml-auto d-block"
|
||||||
|
id="advanced-toggle"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target=".advanced-field-group"
|
||||||
|
aria-expanded="false">
|
||||||
|
<i class="bi bi-gear"></i> {% translate "Show Advanced Options" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||||
{% for fieldset in form.get_fieldsets %}
|
{% for fieldset in form.get_fieldsets %}
|
||||||
{% if not fieldset.hidden %}
|
{% if not fieldset.hidden %}
|
||||||
|
|
@ -54,3 +67,4 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<script defer src="{% static 'js/advanced-fields.js' %}"></script>
|
||||||
|
|
|
||||||
|
|
@ -302,3 +302,33 @@ html[data-bs-theme="dark"] .crd-form .nav-tabs .nav-link .mandatory-indicator {
|
||||||
pointer-events: none;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
83
src/servala/static/js/advanced-fields.js
Normal file
83
src/servala/static/js/advanced-fields.js
Normal file
|
|
@ -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 = '<i class="bi bi-gear-fill"></i> 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 = '<i class="bi bi-gear"></i> 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
Loading…
Add table
Add a link
Reference in a new issue