snapshot: preserve builder deletion and onboarding ux improvements
This commit is contained in:
@@ -977,6 +977,14 @@ body {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.builder-card-head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.builder-entity-card-head strong {
|
||||
color: #142033;
|
||||
font-size: 15px;
|
||||
@@ -1003,6 +1011,7 @@ body {
|
||||
.builder-switch-stack {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
justify-items: end;
|
||||
}
|
||||
|
||||
.builder-switch input[type='checkbox'],
|
||||
@@ -1031,61 +1040,62 @@ body {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section-rule-grid.drag-over {
|
||||
outline: 1px dashed #9db4d2;
|
||||
outline-offset: 6px;
|
||||
}
|
||||
|
||||
.section-rule-card {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 13px 14px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #d6e0ec;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, #f9fbff, #ffffff);
|
||||
cursor: move;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, #fbfdff, #ffffff);
|
||||
transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-rule-card:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.06);
|
||||
border-color: #b8cae0;
|
||||
box-shadow: 0 14px 24px rgba(15, 23, 42, 0.07);
|
||||
border-color: #b2c6df;
|
||||
}
|
||||
|
||||
.section-rule-card.is-locked {
|
||||
background: linear-gradient(180deg, #f4f7fb, #fafcff);
|
||||
background: linear-gradient(180deg, #f5f8fc, #fbfdff);
|
||||
}
|
||||
|
||||
.section-rule-card.dragging {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.section-rule-card.manual-dragging {
|
||||
opacity: 0.72;
|
||||
border-color: #8fb1d8;
|
||||
box-shadow: 0 16px 30px rgba(15, 23, 42, 0.10);
|
||||
}
|
||||
|
||||
.section-rule-drag {
|
||||
color: #8aa0be;
|
||||
font-size: 18px;
|
||||
letter-spacing: -2px;
|
||||
user-select: none;
|
||||
align-self: stretch;
|
||||
.section-rule-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding-right: 2px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
body.builder-dragging,
|
||||
body.builder-dragging * {
|
||||
cursor: grabbing !important;
|
||||
user-select: none !important;
|
||||
.section-move-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid #cdd9e8;
|
||||
border-radius: 11px;
|
||||
background: linear-gradient(180deg, #ffffff, #f5f9ff);
|
||||
color: #274264;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 6px 12px rgba(15, 23, 42, 0.04);
|
||||
transition: transform 0.16s ease, border-color 0.16s ease, background-color 0.16s ease, box-shadow 0.16s ease;
|
||||
}
|
||||
|
||||
.section-move-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: #9db4d2;
|
||||
background: linear-gradient(180deg, #ffffff, #eef5ff);
|
||||
box-shadow: 0 10px 16px rgba(15, 23, 42, 0.07);
|
||||
}
|
||||
|
||||
.section-move-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.section-rule-copy {
|
||||
@@ -1096,7 +1106,8 @@ body.builder-dragging * {
|
||||
|
||||
.section-rule-copy strong {
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
|
||||
@@ -192,6 +192,10 @@ h1 {
|
||||
}
|
||||
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px dashed #dde4f1;
|
||||
padding-bottom: 8px;
|
||||
@@ -210,6 +214,9 @@ h1 {
|
||||
}
|
||||
|
||||
.section-itsetup .section-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin: -14px -14px 16px;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid #d5e2f9;
|
||||
@@ -348,6 +355,11 @@ h1 {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.section-toggle-btn {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.itsetup-checklist-body {
|
||||
padding: 0;
|
||||
background: #ffffff;
|
||||
|
||||
@@ -148,92 +148,36 @@
|
||||
|
||||
const sectionRuleGrid = document.getElementById('section-rule-grid');
|
||||
if (sectionRuleGrid) {
|
||||
let draggingSectionCard = null;
|
||||
let manualDraggingSectionCard = null;
|
||||
|
||||
function getSectionInsertBeforeNode(mouseY) {
|
||||
const cards = Array.from(sectionRuleGrid.querySelectorAll('.section-rule-card:not(.dragging)'));
|
||||
return cards.find((card) => {
|
||||
const box = card.getBoundingClientRect();
|
||||
return mouseY < box.top + box.height / 2;
|
||||
function updateSectionMoveButtons() {
|
||||
const cards = Array.from(sectionRuleGrid.querySelectorAll('.section-rule-card'));
|
||||
cards.forEach((card, index) => {
|
||||
const upBtn = card.querySelector('[data-move-section="up"]');
|
||||
const downBtn = card.querySelector('[data-move-section="down"]');
|
||||
if (upBtn) upBtn.disabled = index === 0;
|
||||
if (downBtn) downBtn.disabled = index === cards.length - 1;
|
||||
});
|
||||
}
|
||||
|
||||
function getManualSectionInsertBeforeNode(mouseY) {
|
||||
const cards = Array.from(sectionRuleGrid.querySelectorAll('.section-rule-card:not(.manual-dragging)'));
|
||||
return cards.find((card) => {
|
||||
const box = card.getBoundingClientRect();
|
||||
return mouseY < box.top + box.height / 2;
|
||||
});
|
||||
}
|
||||
|
||||
function onManualSectionMove(event) {
|
||||
if (!manualDraggingSectionCard) return;
|
||||
event.preventDefault();
|
||||
sectionRuleGrid.classList.add('drag-over');
|
||||
const beforeNode = getManualSectionInsertBeforeNode(event.clientY);
|
||||
if (beforeNode) {
|
||||
sectionRuleGrid.insertBefore(manualDraggingSectionCard, beforeNode);
|
||||
} else {
|
||||
sectionRuleGrid.appendChild(manualDraggingSectionCard);
|
||||
}
|
||||
}
|
||||
|
||||
function endManualSectionDrag() {
|
||||
if (!manualDraggingSectionCard) return;
|
||||
manualDraggingSectionCard.classList.remove('manual-dragging');
|
||||
manualDraggingSectionCard = null;
|
||||
sectionRuleGrid.classList.remove('drag-over');
|
||||
document.body.classList.remove('builder-dragging');
|
||||
document.removeEventListener('mousemove', onManualSectionMove);
|
||||
document.removeEventListener('mouseup', endManualSectionDrag);
|
||||
}
|
||||
|
||||
sectionRuleGrid.querySelectorAll('.section-rule-card').forEach((card) => {
|
||||
card.addEventListener('dragstart', (event) => {
|
||||
draggingSectionCard = card;
|
||||
card.classList.add('dragging');
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', card.dataset.sectionKey || '');
|
||||
});
|
||||
card.addEventListener('dragend', () => {
|
||||
card.classList.remove('dragging');
|
||||
sectionRuleGrid.classList.remove('drag-over');
|
||||
draggingSectionCard = null;
|
||||
});
|
||||
const handle = card.querySelector('.section-rule-drag');
|
||||
if (handle) {
|
||||
handle.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
manualDraggingSectionCard = card;
|
||||
card.classList.add('manual-dragging');
|
||||
sectionRuleGrid.classList.add('drag-over');
|
||||
document.body.classList.add('builder-dragging');
|
||||
document.addEventListener('mousemove', onManualSectionMove);
|
||||
document.addEventListener('mouseup', endManualSectionDrag);
|
||||
});
|
||||
sectionRuleGrid.addEventListener('click', (event) => {
|
||||
const button = event.target.closest('[data-move-section]');
|
||||
if (!button) return;
|
||||
const card = button.closest('.section-rule-card');
|
||||
if (!card) return;
|
||||
const direction = button.dataset.moveSection;
|
||||
if (direction === 'up') {
|
||||
const previousCard = card.previousElementSibling;
|
||||
if (previousCard) {
|
||||
sectionRuleGrid.insertBefore(card, previousCard);
|
||||
}
|
||||
} else if (direction === 'down') {
|
||||
const nextCard = card.nextElementSibling;
|
||||
if (nextCard) {
|
||||
sectionRuleGrid.insertBefore(nextCard, card);
|
||||
}
|
||||
}
|
||||
updateSectionMoveButtons();
|
||||
});
|
||||
|
||||
sectionRuleGrid.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
sectionRuleGrid.classList.add('drag-over');
|
||||
if (!draggingSectionCard) return;
|
||||
const beforeNode = getSectionInsertBeforeNode(event.clientY);
|
||||
if (beforeNode) {
|
||||
sectionRuleGrid.insertBefore(draggingSectionCard, beforeNode);
|
||||
} else {
|
||||
sectionRuleGrid.appendChild(draggingSectionCard);
|
||||
}
|
||||
});
|
||||
|
||||
sectionRuleGrid.addEventListener('dragleave', () => {
|
||||
sectionRuleGrid.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
sectionRuleGrid.addEventListener('drop', (event) => {
|
||||
event.preventDefault();
|
||||
sectionRuleGrid.classList.remove('drag-over');
|
||||
});
|
||||
updateSectionMoveButtons();
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -222,6 +222,45 @@
|
||||
});
|
||||
}
|
||||
|
||||
function setupSectionCheckboxToggles() {
|
||||
document.querySelectorAll('[data-section-checkbox-toggle]').forEach(function (button) {
|
||||
const sectionCard = button.closest('.section-card');
|
||||
if (!sectionCard) return;
|
||||
|
||||
const getCheckboxes = function () {
|
||||
return Array.from(sectionCard.querySelectorAll('.custom-section-checkbox input[type="checkbox"]'));
|
||||
};
|
||||
|
||||
const refreshButtonLabel = function () {
|
||||
const checkboxes = getCheckboxes();
|
||||
if (!checkboxes.length) {
|
||||
button.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
button.classList.remove('hidden');
|
||||
const allChecked = checkboxes.every(function (box) { return box.checked; });
|
||||
button.textContent = allChecked ? (button.dataset.labelClear || 'Auswahl aufheben') : (button.dataset.labelSelect || 'Alle auswählen');
|
||||
};
|
||||
|
||||
button.addEventListener('click', function () {
|
||||
const checkboxes = getCheckboxes();
|
||||
if (!checkboxes.length) return;
|
||||
const shouldCheck = checkboxes.some(function (box) { return !box.checked; });
|
||||
checkboxes.forEach(function (box) {
|
||||
box.checked = shouldCheck;
|
||||
box.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
refreshButtonLabel();
|
||||
});
|
||||
|
||||
getCheckboxes().forEach(function (box) {
|
||||
box.addEventListener('change', refreshButtonLabel);
|
||||
});
|
||||
|
||||
refreshButtonLabel();
|
||||
});
|
||||
}
|
||||
|
||||
function setupChecklistColumns() {
|
||||
document.querySelectorAll('.itsetup-checklist-body > [id^="id_"]').forEach(function (container) {
|
||||
const itemCount = container.querySelectorAll(':scope > div').length;
|
||||
@@ -271,6 +310,7 @@
|
||||
setupWorkEmailAutofill();
|
||||
setupBusinessCardAutofill();
|
||||
setupChecklistToggles();
|
||||
setupSectionCheckboxToggles();
|
||||
setupChecklistColumns();
|
||||
jumpToFirstErrorPage();
|
||||
updateStep();
|
||||
|
||||
@@ -98,11 +98,17 @@
|
||||
{% for section in section_rule_items %}
|
||||
<article
|
||||
class="section-rule-card{% if section.locked %} is-locked{% endif %}"
|
||||
draggable="true"
|
||||
data-section-key="{{ section.key }}"
|
||||
>
|
||||
<input type="hidden" name="section_order" value="{{ section.key }}" />
|
||||
<div class="section-rule-drag" aria-hidden="true" title="{% trans 'Zum Verschieben ziehen' %}">⋮⋮</div>
|
||||
<div class="section-rule-actions">
|
||||
<button class="section-move-btn" type="button" data-move-section="up" aria-label="{% trans 'Nach oben' %}" title="{% trans 'Nach oben' %}">
|
||||
<span aria-hidden="true">↑</span>
|
||||
</button>
|
||||
<button class="section-move-btn" type="button" data-move-section="down" aria-label="{% trans 'Nach unten' %}" title="{% trans 'Nach unten' %}">
|
||||
<span aria-hidden="true">↓</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-rule-copy">
|
||||
<strong>{{ section.title }}</strong>
|
||||
<span>{% blocktrans trimmed with count=section.field_count %}{{ count }} Feld/Felder in diesem Abschnitt.{% endblocktrans %}</span>
|
||||
@@ -491,10 +497,13 @@
|
||||
<strong>{{ item.title }}</strong>
|
||||
<div class="entity-meta">{{ item.section_key }}</div>
|
||||
</div>
|
||||
<label class="builder-switch">
|
||||
<input type="checkbox" name="custom_section_is_active_{{ item.id }}" {% if item.is_active %}checked{% endif %} />
|
||||
<span>{% trans "Aktiv" %}</span>
|
||||
</label>
|
||||
<div class="builder-card-head-actions">
|
||||
<label class="builder-switch">
|
||||
<input type="checkbox" name="custom_section_is_active_{{ item.id }}" {% if item.is_active %}checked{% endif %} />
|
||||
<span>{% trans "Aktiv" %}</span>
|
||||
</label>
|
||||
<button class="btn btn-secondary" type="submit" name="delete_custom_section_id" value="{{ item.id }}" data-confirm="{% trans 'Eigenen Abschnitt wirklich löschen? Zugehörige eigene Felder werden ebenfalls entfernt.' %}">{% trans "Löschen" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="builder-entity-grid">
|
||||
<label class="builder-entity-control">
|
||||
@@ -613,7 +622,7 @@
|
||||
<strong>{{ item.label }}</strong>
|
||||
<div class="entity-meta">{{ item.field_key }}</div>
|
||||
</div>
|
||||
<div class="builder-switch-stack">
|
||||
<div class="builder-switch-stack">
|
||||
<label class="builder-switch">
|
||||
<input type="checkbox" name="custom_is_required_{{ item.id }}" {% if item.is_required %}checked{% endif %} />
|
||||
<span>{% trans "Pflicht" %}</span>
|
||||
@@ -622,8 +631,9 @@
|
||||
<input type="checkbox" name="custom_is_active_{{ item.id }}" {% if item.is_active %}checked{% endif %} />
|
||||
<span>{% trans "Aktiv" %}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" type="submit" name="delete_custom_field_id" value="{{ item.id }}" data-confirm="{% trans 'Eigenes Feld wirklich löschen?' %}">{% trans "Löschen" %}</button>
|
||||
</div>
|
||||
<div class="builder-entity-grid">
|
||||
<label class="builder-entity-control">
|
||||
<span>{% trans "Abschnitt" %}</span>
|
||||
@@ -670,9 +680,6 @@
|
||||
<textarea name="custom_select_options_en_{{ item.id }}" rows="2">{{ item.select_options_en }}</textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="builder-entity-card-actions">
|
||||
<button class="btn btn-secondary" type="submit" name="delete_custom_field_id" value="{{ item.id }}" data-confirm="{% trans 'Eigenes Feld wirklich löschen?' %}">{% trans "Löschen" %}</button>
|
||||
</div>
|
||||
</article>
|
||||
{% empty %}
|
||||
<div class="builder-empty-state">{% trans "Keine eigenen Felder vorhanden." %}</div>
|
||||
|
||||
@@ -50,8 +50,19 @@
|
||||
<section class="page {% if forloop.first %}active{% endif %}" data-step="{{ forloop.counter }}">
|
||||
<div class="section-card section-{{ section.key }}">
|
||||
<div class="section-head">
|
||||
<h2>{{ section.title }}</h2>
|
||||
<p>{{ section.subtitle }}</p>
|
||||
<div>
|
||||
<h2>{{ section.title }}</h2>
|
||||
<p>{{ section.subtitle }}</p>
|
||||
</div>
|
||||
{% if section.has_custom_checkbox_fields %}
|
||||
<button
|
||||
type="button"
|
||||
class="checklist-toggle-btn section-toggle-btn"
|
||||
data-section-checkbox-toggle
|
||||
data-label-select="{% trans 'Alle auswählen' %}"
|
||||
data-label-clear="{% trans 'Auswahl aufheben' %}"
|
||||
>{% trans "Alle auswählen" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid-2">
|
||||
{% for block in section.blocks %}
|
||||
@@ -78,7 +89,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% elif field.name in onboarding_inline_checks or field.field.widget.input_type == 'checkbox' %}
|
||||
<div class="field inline-check field-full {% if section.key == 'abschluss' %}finish-check{% endif %}">
|
||||
<div class="field inline-check field-full {% if section.key == 'abschluss' %}finish-check{% endif %} {% if field.name|slice:':8' == 'custom__' and field.field.widget.input_type == 'checkbox' %}custom-section-checkbox{% endif %}">
|
||||
{{ field }} {{ field.label_tag }}
|
||||
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
|
||||
{{ field.errors }}
|
||||
@@ -121,7 +132,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% elif field.name in onboarding_inline_checks or field.field.widget.input_type == 'checkbox' %}
|
||||
<div class="field inline-check field-full {% if section.key == 'abschluss' %}finish-check{% endif %}">
|
||||
<div class="field inline-check field-full {% if section.key == 'abschluss' %}finish-check{% endif %} {% if field.name|slice:':8' == 'custom__' and field.field.widget.input_type == 'checkbox' %}custom-section-checkbox{% endif %}">
|
||||
{{ field }} {{ field.label_tag }}
|
||||
{% if field.help_text %}<div class="hint">{{ field.help_text }}</div>{% endif %}
|
||||
{{ field.errors }}
|
||||
|
||||
@@ -331,6 +331,60 @@ class FormBuilderAdminTests(TestCase):
|
||||
self.assertEqual(section.title, 'Benefits')
|
||||
self.assertEqual(section.sort_order, 5)
|
||||
|
||||
def test_staff_can_delete_custom_field(self):
|
||||
self.client.force_login(self.staff)
|
||||
field = FormCustomFieldConfig.objects.create(
|
||||
form_type='onboarding',
|
||||
field_key='laptop_tag',
|
||||
section_key='itsetup',
|
||||
sort_order=0,
|
||||
field_type='text',
|
||||
label='Laptop-Tag',
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
'/admin-tools/form-builder/?form_type=onboarding&option_category=device',
|
||||
data={'delete_custom_field_id': str(field.id)},
|
||||
HTTP_HOST='localhost',
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertFalse(FormCustomFieldConfig.objects.filter(id=field.id).exists())
|
||||
|
||||
def test_staff_can_delete_custom_section_and_its_fields(self):
|
||||
self.client.force_login(self.staff)
|
||||
section = FormCustomSectionConfig.objects.create(
|
||||
form_type='onboarding',
|
||||
section_key='benefits',
|
||||
sort_order=0,
|
||||
title='Benefits',
|
||||
)
|
||||
field = FormCustomFieldConfig.objects.create(
|
||||
form_type='onboarding',
|
||||
field_key='meal_allowance',
|
||||
section_key='benefits',
|
||||
sort_order=0,
|
||||
field_type='checkbox',
|
||||
label='Essenszuschuss',
|
||||
)
|
||||
FormConditionalRuleConfig.objects.create(
|
||||
form_type='onboarding',
|
||||
target_key='custom__meal_allowance',
|
||||
clauses=[{'field': 'employment_type', 'operator': 'equals', 'value': 'unbefristet'}],
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
'/admin-tools/form-builder/?form_type=onboarding&option_category=device',
|
||||
data={'delete_custom_section_id': str(section.id)},
|
||||
HTTP_HOST='localhost',
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertFalse(FormCustomSectionConfig.objects.filter(id=section.id).exists())
|
||||
self.assertFalse(FormCustomFieldConfig.objects.filter(id=field.id).exists())
|
||||
self.assertFalse(FormConditionalRuleConfig.objects.filter(target_key='custom__meal_allowance').exists())
|
||||
|
||||
def test_save_order_accepts_custom_section_column(self):
|
||||
self.client.force_login(self.staff)
|
||||
FormCustomSectionConfig.objects.create(
|
||||
|
||||
@@ -226,6 +226,41 @@ class OnboardingFlowTests(TestCase):
|
||||
self.assertIn('Benefits', html)
|
||||
self.assertIn('Essenszuschuss', html)
|
||||
|
||||
def test_onboarding_custom_section_with_checkbox_fields_shows_section_select_all(self):
|
||||
FormCustomSectionConfig.objects.create(
|
||||
form_type='onboarding',
|
||||
section_key='benefits',
|
||||
sort_order=10,
|
||||
title='Benefits',
|
||||
is_active=True,
|
||||
)
|
||||
FormCustomFieldConfig.objects.create(
|
||||
form_type='onboarding',
|
||||
field_key='meal_allowance',
|
||||
section_key='benefits',
|
||||
sort_order=0,
|
||||
field_type='checkbox',
|
||||
is_active=True,
|
||||
label='Essenszuschuss',
|
||||
)
|
||||
FormCustomFieldConfig.objects.create(
|
||||
form_type='onboarding',
|
||||
field_key='parking_spot',
|
||||
section_key='benefits',
|
||||
sort_order=1,
|
||||
field_type='checkbox',
|
||||
is_active=True,
|
||||
label='Parkplatz',
|
||||
)
|
||||
|
||||
response = self.client.get('/onboarding/new/', HTTP_HOST='localhost')
|
||||
html = response.content.decode('utf-8')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('data-section-checkbox-toggle', html)
|
||||
self.assertIn('Essenszuschuss', html)
|
||||
self.assertIn('Parkplatz', html)
|
||||
|
||||
@patch('workflows.views.process_onboarding_request.delay')
|
||||
def test_onboarding_custom_field_is_rendered_and_saved(self, mock_delay):
|
||||
FormCustomFieldConfig.objects.create(
|
||||
|
||||
@@ -610,16 +610,33 @@ def _build_onboarding_sections(blocks: list[dict], field_pages: dict[str, str],
|
||||
section_key = 'abschluss'
|
||||
grouped[section_key].append(block)
|
||||
visible_keys = visible_section_keys or set(section_order)
|
||||
return [
|
||||
{
|
||||
'key': key,
|
||||
'title': section_titles.get(key, ONBOARDING_SECTION_META.get(key, {}).get('title', key)),
|
||||
'subtitle': ONBOARDING_SECTION_META.get(key, {}).get('subtitle', ''),
|
||||
'blocks': grouped[key],
|
||||
}
|
||||
for key in section_order
|
||||
if key in visible_keys
|
||||
]
|
||||
sections = []
|
||||
custom_section_keys = {item['key'] for item in section_defs if item.get('is_custom')}
|
||||
for key in section_order:
|
||||
if key not in visible_keys:
|
||||
continue
|
||||
blocks_for_section = grouped[key]
|
||||
has_custom_checkbox_fields = False
|
||||
for block in blocks_for_section:
|
||||
candidate_fields = [block['field']] if block['kind'] == 'field' else (block.get('fields') or [])
|
||||
for bound_field in candidate_fields:
|
||||
widget_type = getattr(getattr(bound_field.field, 'widget', None), 'input_type', '')
|
||||
if bound_field.name.startswith('custom__') and widget_type == 'checkbox':
|
||||
has_custom_checkbox_fields = True
|
||||
break
|
||||
if has_custom_checkbox_fields:
|
||||
break
|
||||
sections.append(
|
||||
{
|
||||
'key': key,
|
||||
'title': section_titles.get(key, ONBOARDING_SECTION_META.get(key, {}).get('title', key)),
|
||||
'subtitle': ONBOARDING_SECTION_META.get(key, {}).get('subtitle', ''),
|
||||
'blocks': blocks_for_section,
|
||||
'is_custom': key in custom_section_keys,
|
||||
'has_custom_checkbox_fields': has_custom_checkbox_fields,
|
||||
}
|
||||
)
|
||||
return sections
|
||||
|
||||
|
||||
OFFBOARDING_SECTION_META = {
|
||||
@@ -2182,6 +2199,7 @@ def form_builder_page(request):
|
||||
if request.method == 'POST':
|
||||
delete_option_id = request.POST.get('delete_option_id', '').strip()
|
||||
delete_custom_field_id = request.POST.get('delete_custom_field_id', '').strip()
|
||||
delete_custom_section_id = request.POST.get('delete_custom_section_id', '').strip()
|
||||
if delete_option_id:
|
||||
option = FormOption.objects.filter(id=delete_option_id).first()
|
||||
if not option:
|
||||
@@ -2205,6 +2223,34 @@ def form_builder_page(request):
|
||||
_audit(request, 'form_custom_field_deleted', target_type='form_custom_field', target_id=deleted_id, target_label=deleted_label)
|
||||
messages.success(request, 'Benutzerdefiniertes Feld wurde gelöscht.')
|
||||
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=custom-fields#builder-content")
|
||||
if delete_custom_section_id:
|
||||
custom_section = FormCustomSectionConfig.objects.filter(id=delete_custom_section_id, form_type=form_type).first()
|
||||
if not custom_section:
|
||||
messages.error(request, 'Benutzerdefinierter Abschnitt nicht gefunden.')
|
||||
else:
|
||||
deleted_label = custom_section.title
|
||||
deleted_id = custom_section.id
|
||||
section_key = custom_section.section_key
|
||||
custom_fields = list(FormCustomFieldConfig.objects.filter(form_type=form_type, section_key=section_key))
|
||||
deleted_field_count = len(custom_fields)
|
||||
if custom_fields:
|
||||
field_keys = [item.field_key for item in custom_fields]
|
||||
FormConditionalRuleConfig.objects.filter(
|
||||
form_type=form_type,
|
||||
target_key__in=[f'custom__{field_key}' for field_key in field_keys],
|
||||
).delete()
|
||||
FormCustomFieldConfig.objects.filter(id__in=[item.id for item in custom_fields]).delete()
|
||||
custom_section.delete()
|
||||
_audit(
|
||||
request,
|
||||
'form_custom_section_deleted',
|
||||
target_type='form_custom_section',
|
||||
target_id=deleted_id,
|
||||
target_label=deleted_label,
|
||||
details={'section_key': section_key, 'deleted_field_count': deleted_field_count},
|
||||
)
|
||||
messages.success(request, 'Benutzerdefinierter Abschnitt wurde gelöscht.')
|
||||
return redirect(f"/admin-tools/form-builder/?form_type={form_type}&option_category={option_category}&panel=builder-content&subpanel=custom-sections#builder-content")
|
||||
|
||||
action = request.POST.get('builder_action', '')
|
||||
if action == 'add_option':
|
||||
|
||||
Reference in New Issue
Block a user