Use custom forms in instance update
This commit is contained in:
parent
0045e532ee
commit
cedcab85c4
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." %}
|
{% translate "Oops! Something went wrong with the service form generation. Please try again later." %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include "includes/tabbed_fieldset_form.html" with form=form %}
|
{% include "includes/tabbed_fieldset_form.html" with form=custom_form expert_form=form %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,48 +6,130 @@
|
||||||
{% 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" %}
|
||||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
{% if form %}
|
||||||
{% for fieldset in form.get_fieldsets %}
|
<div class="mb-3">
|
||||||
{% if not fieldset.hidden %}
|
<button type="button"
|
||||||
<li class="nav-item"
|
class="btn btn-sm btn-outline-secondary ml-auto d-block"
|
||||||
role="presentation">
|
id="expert-mode-toggle">
|
||||||
<button class="nav-link {% if forloop.first %}active{% endif %}{% if fieldset.has_mandatory %} has-mandatory{% endif %}"
|
<i class="bi bi-code-square"></i> {% translate "Show Expert Mode" %}
|
||||||
id="{{ fieldset.title|slugify }}-tab"
|
</button>
|
||||||
data-bs-toggle="tab"
|
</div>
|
||||||
data-bs-target="#{{ fieldset.title|slugify }}"
|
{% endif %}
|
||||||
type="button"
|
<div id="custom-form-container" class="{% if form %}custom-crd-form{% else %}expert-crd-form{% endif %}">
|
||||||
role="tab"
|
{% if form and form.get_fieldsets|length == 1 %}
|
||||||
aria-controls="{{ fieldset.title|slugify }}"
|
{# Single fieldset - render without tabs #}
|
||||||
aria-selected="{% if forloop.first %}true{% else %}false{% endif %}">
|
{% for fieldset in form.get_fieldsets %}
|
||||||
{{ fieldset.title }}
|
<div class="my-2">
|
||||||
{% if fieldset.has_mandatory %}<span class="mandatory-indicator">*</span>{% endif %}
|
{% for field in fieldset.fields %}
|
||||||
</button>
|
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||||
</li>
|
{% endfor %}
|
||||||
{% endif %}
|
{% for subfieldset in fieldset.fieldsets %}
|
||||||
{% endfor %}
|
{% if subfieldset.fields %}
|
||||||
</ul>
|
<div>
|
||||||
<div class="tab-content" id="myTabContent">
|
<h4 class="mt-3">{{ subfieldset.title }}</h4>
|
||||||
{% for fieldset in form.get_fieldsets %}
|
{% for field in subfieldset.fields %}
|
||||||
<div class="tab-pane fade my-2 {% if fieldset.hidden %}d-none{% endif %}{% if forloop.first %}show active{% endif %}"
|
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
||||||
id="{{ fieldset.title|slugify }}"
|
{% endfor %}
|
||||||
role="tabpanel"
|
</div>
|
||||||
aria-labelledby="{{ fieldset.title|slugify }}-tab">
|
{% endif %}
|
||||||
{% for field in fieldset.fields %}
|
{% endfor %}
|
||||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for subfieldset in fieldset.fieldsets %}
|
{% elif form %}
|
||||||
{% if subfieldset.fields %}
|
{# Multiple fieldsets or auto-generated form - render with tabs #}
|
||||||
<div>
|
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||||
<h4 class="mt-3">{{ subfieldset.title }}</h4>
|
{% for fieldset in form.get_fieldsets %}
|
||||||
{% for field in subfieldset.fields %}
|
{% if not fieldset.hidden %}
|
||||||
{% with field=form|get_field:field %}{{ field.as_field_group }}{% endwith %}
|
<li class="nav-item"
|
||||||
{% endfor %}
|
role="presentation">
|
||||||
</div>
|
<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 %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% 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>
|
</div>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
</div>
|
</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">
|
<div class="col-sm-12 d-flex justify-content-end">
|
||||||
<button class="btn btn-primary me-1 mb-1" type="submit">
|
<button class="btn btn-primary me-1 mb-1" type="submit">
|
||||||
{% if form_submit_label %}
|
{% if form_submit_label %}
|
||||||
|
|
@ -58,3 +140,6 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{% if form %}
|
||||||
|
<script defer src="{% static 'js/expert-mode.js' %}"></script>
|
||||||
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -393,11 +393,75 @@ class ServiceInstanceUpdateView(
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
kwargs["instance"] = self.object.spec_object
|
kwargs["instance"] = self.object.spec_object
|
||||||
|
kwargs["prefix"] = "expert"
|
||||||
return kwargs
|
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):
|
def form_valid(self, form):
|
||||||
try:
|
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)
|
self.object.update_spec(spec_data=spec_data, updated_by=self.request.user)
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request,
|
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