Custom form configuration #268
4 changed files with 237 additions and 40 deletions
|
|
@ -22,7 +22,7 @@
|
|||
{% translate "Oops! Something went wrong with the service form generation. Please try again later." %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% include "includes/tabbed_fieldset_form.html" with form=form %}
|
||||
{% include "includes/tabbed_fieldset_form.html" with form=custom_form expert_form=form %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,48 +6,130 @@
|
|||
{% if form_action %}action="{{ form_action }}"{% endif %}>
|
||||
{% csrf_token %}
|
||||
{% include "frontend/forms/errors.html" %}
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
{% for fieldset in form.get_fieldsets %}
|
||||
{% if not fieldset.hidden %}
|
||||
<li class="nav-item"
|
||||
role="presentation">
|
||||
<button class="nav-link {% if forloop.first %}active{% endif %}{% if fieldset.has_mandatory %} has-mandatory{% endif %}"
|
||||
id="{{ fieldset.title|slugify }}-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#{{ fieldset.title|slugify }}"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="{{ fieldset.title|slugify }}"
|
||||
aria-selected="{% if forloop.first %}true{% else %}false{% endif %}">
|
||||
{{ fieldset.title }}
|
||||
{% if fieldset.has_mandatory %}<span class="mandatory-indicator">*</span>{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
{% for fieldset in form.get_fieldsets %}
|
||||
<div class="tab-pane fade my-2 {% if fieldset.hidden %}d-none{% endif %}{% if forloop.first %}show active{% endif %}"
|
||||
id="{{ fieldset.title|slugify }}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="{{ fieldset.title|slugify }}-tab">
|
||||
{% for field in fieldset.fields %}
|
||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
{% for subfieldset in fieldset.fieldsets %}
|
||||
{% if subfieldset.fields %}
|
||||
<div>
|
||||
<h4 class="mt-3">{{ subfieldset.title }}</h4>
|
||||
{% for field in subfieldset.fields %}
|
||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if form %}
|
||||
<div class="mb-3">
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-secondary ml-auto d-block"
|
||||
id="expert-mode-toggle">
|
||||
<i class="bi bi-code-square"></i> {% translate "Show Expert Mode" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="custom-form-container" class="{% if form %}custom-crd-form{% else %}expert-crd-form{% endif %}">
|
||||
{% if form and form.get_fieldsets|length == 1 %}
|
||||
{# Single fieldset - render without tabs #}
|
||||
{% for fieldset in form.get_fieldsets %}
|
||||
<div class="my-2">
|
||||
{% for field in fieldset.fields %}
|
||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
{% for subfieldset in fieldset.fieldsets %}
|
||||
{% if subfieldset.fields %}
|
||||
<div>
|
||||
<h4 class="mt-3">{{ subfieldset.title }}</h4>
|
||||
{% for field in subfieldset.fields %}
|
||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% elif form %}
|
||||
{# Multiple fieldsets or auto-generated form - render with tabs #}
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
{% for fieldset in form.get_fieldsets %}
|
||||
{% if not fieldset.hidden %}
|
||||
<li class="nav-item"
|
||||
role="presentation">
|
||||
<button class="nav-link {% if forloop.first %}active{% endif %}{% if fieldset.has_mandatory %} has-mandatory{% endif %}"
|
||||
id="{{ fieldset.title|slugify }}-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#{{ fieldset.title|slugify }}"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="{{ fieldset.title|slugify }}"
|
||||
aria-selected="{% if forloop.first %}true{% else %}false{% endif %}">
|
||||
{{ fieldset.title }}
|
||||
{% if fieldset.has_mandatory %}<span class="mandatory-indicator">*</span>{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
{% for fieldset in form.get_fieldsets %}
|
||||
<div class="tab-pane fade my-2 {% if fieldset.hidden %}d-none{% endif %}{% if forloop.first %}show active{% endif %}"
|
||||
id="{{ fieldset.title|slugify }}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="{{ fieldset.title|slugify }}-tab">
|
||||
{% for field in fieldset.fields %}
|
||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
{% for subfieldset in fieldset.fieldsets %}
|
||||
{% if subfieldset.fields %}
|
||||
<div>
|
||||
<h4 class="mt-3">{{ subfieldset.title }}</h4>
|
||||
{% for field in subfieldset.fields %}
|
||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if form and expert_form %}
|
||||
<div id="expert-form-container" class="expert-crd-form" style="display:none;">
|
||||
<ul class="nav nav-tabs" id="expertTab" role="tablist">
|
||||
{% for fieldset in expert_form.get_fieldsets %}
|
||||
{% if not fieldset.hidden %}
|
||||
<li class="nav-item"
|
||||
role="presentation">
|
||||
<button class="nav-link {% if forloop.first %}active{% endif %}{% if fieldset.has_mandatory %} has-mandatory{% endif %}"
|
||||
id="expert-{{ fieldset.title|slugify }}-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#expert-{{ fieldset.title|slugify }}"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="expert-{{ fieldset.title|slugify }}"
|
||||
aria-selected="{% if forloop.first %}true{% else %}false{% endif %}">
|
||||
{{ fieldset.title }}
|
||||
{% if fieldset.has_mandatory %}<span class="mandatory-indicator">*</span>{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="tab-content" id="expertTabContent">
|
||||
{% for fieldset in expert_form.get_fieldsets %}
|
||||
<div class="tab-pane fade my-2 {% if fieldset.hidden %}d-none{% endif %}{% if forloop.first %}show active{% endif %}"
|
||||
id="expert-{{ fieldset.title|slugify }}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="expert-{{ fieldset.title|slugify }}-tab">
|
||||
{% for field in fieldset.fields %}
|
||||
{% with field=expert_form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
{% for subfieldset in fieldset.fieldsets %}
|
||||
{% if subfieldset.fields %}
|
||||
<div>
|
||||
<h4 class="mt-3">{{ subfieldset.title }}</h4>
|
||||
{% for field in subfieldset.fields %}
|
||||
{% with field=expert_form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if form %}
|
||||
<input type="hidden" name="active_form" id="active-form-input" value="custom">
|
||||
{% endif %}
|
||||
<div class="col-sm-12 d-flex justify-content-end">
|
||||
<button class="btn btn-primary me-1 mb-1" type="submit">
|
||||
{% if form_submit_label %}
|
||||
|
|
@ -58,3 +140,6 @@
|
|||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if form %}
|
||||
<script defer src="{% static 'js/expert-mode.js' %}"></script>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -393,11 +393,75 @@ class ServiceInstanceUpdateView(
|
|||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["instance"] = self.object.spec_object
|
||||
kwargs["prefix"] = "expert"
|
||||
return kwargs
|
||||
|
||||
def get_form(self, *args, ignore_data=False, **kwargs):
|
||||
if not ignore_data:
|
||||
return super().get_form(*args, **kwargs)
|
||||
cls = self.get_form_class()
|
||||
kwargs = self.get_form_kwargs()
|
||||
if ignore_data:
|
||||
kwargs.pop("data", None)
|
||||
return cls(**kwargs)
|
||||
|
||||
def get_custom_form(self, ignore_data=False):
|
||||
cls = self.object.context.custom_model_form_class
|
||||
if not cls:
|
||||
return
|
||||
kwargs = self.get_form_kwargs()
|
||||
kwargs["prefix"] = "custom"
|
||||
if ignore_data:
|
||||
kwargs.pop("data", None)
|
||||
return cls(**kwargs)
|
||||
|
||||
@property
|
||||
def is_custom_form(self):
|
||||
# Note: "custom form" = user-friendly, subset of fields
|
||||
# vs "expert form" = auto-generated (all technical fields)
|
||||
return self.request.POST.get("active_form", "expert") == "custom"
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
|
||||
if self.is_custom_form:
|
||||
form = self.get_custom_form()
|
||||
else:
|
||||
form = self.get_form()
|
||||
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
return self.form_invalid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.request.method == "POST":
|
||||
if self.is_custom_form:
|
||||
context["custom_form"] = self.get_custom_form()
|
||||
context["form"] = self.get_form(ignore_data=True)
|
||||
else:
|
||||
context["custom_form"] = self.get_custom_form(ignore_data=True)
|
||||
else:
|
||||
context["custom_form"] = self.get_custom_form()
|
||||
return context
|
||||
|
||||
def _deep_merge(self, base, update):
|
||||
for key, value in update.items():
|
||||
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
|
||||
self._deep_merge(base[key], value)
|
||||
else:
|
||||
base[key] = value
|
||||
return base
|
||||
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
spec_data = form.get_nested_data().get("spec")
|
||||
form_data = form.get_nested_data()
|
||||
spec_data = form_data.get("spec")
|
||||
|
||||
if self.is_custom_form:
|
||||
current_spec = dict(self.object.spec) if self.object.spec else {}
|
||||
spec_data = self._deep_merge(current_spec, spec_data)
|
||||
|
||||
self.object.update_spec(spec_data=spec_data, updated_by=self.request.user)
|
||||
messages.success(
|
||||
self.request,
|
||||
|
|
|
|||
48
src/servala/static/js/expert-mode.js
Normal file
48
src/servala/static/js/expert-mode.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
(function() {
|
||||
'use strict';
|
||||
|
||||
let isExpertMode = false;
|
||||
|
||||
function initExpertMode() {
|
||||
const toggleButton = document.getElementById('expert-mode-toggle');
|
||||
if (!toggleButton) return;
|
||||
|
||||
const customFormContainer = document.getElementById('custom-form-container');
|
||||
const expertFormContainer = document.getElementById('expert-form-container');
|
||||
|
||||
if (!customFormContainer || !expertFormContainer) {
|
||||
console.warn('Expert mode containers not found');
|
||||
return;
|
||||
}
|
||||
|
||||
toggleButton.addEventListener('click', function() {
|
||||
isExpertMode = !isExpertMode;
|
||||
|
||||
const activeFormInput = document.getElementById('active-form-input');
|
||||
|
||||
if (isExpertMode) {
|
||||
customFormContainer.style.display = 'none';
|
||||
expertFormContainer.style.display = 'block';
|
||||
toggleButton.innerHTML = '<i class="bi bi-code-square-fill"></i> Show Simplified Form';
|
||||
if (activeFormInput) activeFormInput.value = 'expert';
|
||||
} else {
|
||||
customFormContainer.style.display = 'block';
|
||||
expertFormContainer.style.display = 'none';
|
||||
toggleButton.innerHTML = '<i class="bi bi-code-square"></i> Show Expert Mode';
|
||||
if (activeFormInput) activeFormInput.value = 'custom';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initExpertMode);
|
||||
} else {
|
||||
initExpertMode();
|
||||
}
|
||||
|
||||
document.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (event.detail.target.id === 'service-form' || event.detail.target.classList.contains('crd-form')) {
|
||||
initExpertMode();
|
||||
}
|
||||
});
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue