From bd055f6e08971aa6b5ce6888b04c0066c2cc0358 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 3 Aug 2026 20:17:54 +0000
Subject: [PATCH 1/3] Initial plan
From d6f516c5f07a29be0515dba811fcc630ad4cdaeb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 3 Aug 2026 20:20:15 +0000
Subject: [PATCH 2/3] Initial plan
Co-authored-by: matteius <479892+matteius@users.noreply.github.com>
---
.../js/form-builder-property-editor.js | 727 ++++++++++++++++++
1 file changed, 727 insertions(+)
create mode 100644 django_forms_workflows/static/django_forms_workflows/js/form-builder-property-editor.js
diff --git a/django_forms_workflows/static/django_forms_workflows/js/form-builder-property-editor.js b/django_forms_workflows/static/django_forms_workflows/js/form-builder-property-editor.js
new file mode 100644
index 0000000..37cb4a0
--- /dev/null
+++ b/django_forms_workflows/static/django_forms_workflows/js/form-builder-property-editor.js
@@ -0,0 +1,727 @@
+ * Property editor for the Form Builder: the field-properties modal (Basic/
+ * Conditional Logic/Validation/Dependencies tabs) and saving those changes
+ * back onto the field.
+ *
+ * Mixed onto FormBuilder.prototype in form-builder.js (Object.assign), not a
+ * standalone class of its own - these methods read/write a large number of
+ * page DOM fields directly, plus `this.fields`/`this.currentFieldIndex`/
+ * `this.isNewField`/`this.config`, and call back into
+ * deleteFieldSilently()/escapeHtml()/renderCanvas()/updatePreview(), which
+ * still live on the single FormBuilder instance.
+ */
+export const propertyEditorMethods = {
+ editField(index, isNew = false) {
+ this.currentFieldIndex = index;
+ this.isNewField = isNew;
+ const field = this.fields[index];
+
+ // Build property form
+ const form = this.buildPropertyForm(field);
+ document.getElementById('fieldPropertyForm').innerHTML = form;
+ this.initializePropertyFormTabs(field);
+
+ // Show modal
+ const modalElement = document.getElementById('fieldPropertyModal');
+ const modal = new bootstrap.Modal(modalElement);
+
+ // Handle modal close/cancel - remove field if it's new and not saved
+ const handleModalClose = () => {
+ if (this.isNewField) {
+ // Field was not saved, remove it
+ this.deleteFieldSilently(this.currentFieldIndex);
+ }
+ this.isNewField = false;
+ // Remove event listener to avoid memory leaks
+ modalElement.removeEventListener('hidden.bs.modal', handleModalClose);
+ };
+
+ // Add event listener for modal close
+ modalElement.addEventListener('hidden.bs.modal', handleModalClose);
+
+ modal.show();
+ },
+
+ buildPropertyForm(field) {
+ const prefillOptions = this.config.prefillSources.map(source =>
+ ``
+ ).join('');
+
+ const widthChoices = [
+ { value: 'full', label: 'Full Width' },
+ { value: 'half', label: 'Half (50%)' },
+ { value: 'third', label: 'One Third (33%)' },
+ { value: 'fourth', label: 'One Quarter (25%)' }
+ ];
+ const widthOptions = widthChoices.map(w =>
+ ``
+ ).join('');
+
+ return `
+
+
+
+
+ ${this.buildBasicPropertiesTab(field, prefillOptions, widthOptions)}
+
+
+
+
+ ${this.buildConditionalLogicTab(field)}
+
+
+
+
+ ${this.buildValidationTab(field)}
+
+
+
+
+ ${this.buildDependenciesTab(field)}
+
+
+ `;
+ },
+
+ buildBasicPropertiesTab(field, prefillOptions, widthOptions) {
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${['select', 'radio', 'checkbox_multiple', 'multiselect', 'multiselect_list', 'checkboxes'].includes(field.field_type) ? `
+
+
+
+ Enter each option on a new line. Use value|Label format for separate values and display text.
+
+ ` : ''}
+ ${field.field_type === 'calculated' ? `
+
+
+
+ Use {field_name} to reference other fields. Evaluated live on the client and re-validated on the server.
+
+ ` : ''}
+ ${field.field_type === 'display_text' ? `
+
+
+
+ Supports Markdown: **bold**, *italic*, [links](url), lists, etc. This text is shown read-only on the form.
+
+ ` : ''}
+ ${field.field_type === 'rating' ? `
+
+
+
+ Number of stars to display (3-10)
+
+ ` : ''}
+ ${field.field_type === 'slider' ? `
+
+
+
+
+
+
+
+
+
+
+
+ Increment value
+
+ ` : ''}
+ ${field.field_type === 'matrix' ? `
+
+
+
+ Define rows and columns as JSON: {"rows": [...], "columns": [...]}
+
+ ` : ''}
+
+
+
+
+ ${['select', 'radio', 'checkbox_multiple', 'multiselect', 'multiselect_list', 'checkboxes'].includes(field.field_type) ? `
+
+
+
+ Centrally managed list — updates apply to all forms using it.
+
+ ` : ''}
+
+
+
+
+
+
+
+ Assign to an approval step for sequential approval workflows
+
+
+ `;
+ },
+
+ buildConditionalLogicTab(field) {
+ // Initialize conditional_rules if not present
+ if (!field.conditional_rules) {
+ field.conditional_rules = null;
+ }
+
+ const conditionalRulesJson = field.conditional_rules ? JSON.stringify(field.conditional_rules, null, 2) : '';
+
+ // Get list of other fields for dropdown
+ const otherFields = this.fields.filter(f => f.field_name !== field.field_name);
+ const fieldOptions = otherFields.map(f =>
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ container.appendChild(row);
+ },
+
+ initializeValidationRulesList(rules) {
+ const container = document.getElementById('validationRulesList');
+ if (!container) return;
+
+ container.innerHTML = '';
+ rules.forEach((rule, index) => {
+ this.addValidationRuleRow(rule, index);
+ });
+
+ // Add event listener for add button
+ const btnAdd = document.getElementById('btnAddValidation');
+ if (btnAdd) {
+ btnAdd.addEventListener('click', () => this.addValidationRuleRow({}, rules.length));
+ }
+ },
+
+ addValidationRuleRow(rule, index) {
+ const container = document.getElementById('validationRulesList');
+ if (!container) return;
+
+ const row = document.createElement('div');
+ row.className = 'card mb-2';
+ row.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ container.appendChild(row);
+ },
+
+ initializeDependenciesList(dependencies) {
+ const container = document.getElementById('dependenciesList');
+ if (!container) return;
+
+ container.innerHTML = '';
+ dependencies.forEach((dep, index) => {
+ this.addDependencyRow(dep, index);
+ });
+
+ // Add event listener for add button
+ const btnAdd = document.getElementById('btnAddDependency');
+ if (btnAdd) {
+ btnAdd.addEventListener('click', () => this.addDependencyRow({}, dependencies.length));
+ }
+ },
+
+ addDependencyRow(dependency, index) {
+ const container = document.getElementById('dependenciesList');
+ if (!container) return;
+
+ const otherFields = this.fields.filter(f => f.field_name !== this.fields[this.currentFieldIndex].field_name);
+ const fieldOptions = otherFields.map(f =>
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ container.appendChild(row);
+ },
+
+ saveFieldProperties() {
+ if (this.currentFieldIndex === null) return;
+
+ const field = this.fields[this.currentFieldIndex];
+
+ // Update basic field properties
+ field.field_label = document.getElementById('propFieldLabel').value;
+ field.field_name = document.getElementById('propFieldName').value;
+ field.required = document.getElementById('propRequired').checked;
+ field.help_text = document.getElementById('propHelpText').value;
+ const showHelpCheckbox = document.getElementById('propShowHelpTextInDetail');
+ field.show_help_text_in_detail = showHelpCheckbox ? showHelpCheckbox.checked : false;
+ field.placeholder = document.getElementById('propPlaceholder').value;
+ field.width = document.getElementById('propWidth').value;
+ field.css_class = document.getElementById('propCssClass').value;
+
+ const prefillSelect = document.getElementById('propPrefillSource');
+ field.prefill_source_id = prefillSelect.value ? parseInt(prefillSelect.value) : null;
+
+ const sharedListSelect = document.getElementById('propSharedOptionList');
+ field.shared_option_list_id = sharedListSelect && sharedListSelect.value ? parseInt(sharedListSelect.value) : null;
+
+ // Save approval step
+ const approvalStepSelect = document.getElementById('propApprovalStep');
+ field.approval_step = approvalStepSelect.value ? parseInt(approvalStepSelect.value) : null;
+
+ const choicesEl = document.getElementById('propChoices');
+ if (choicesEl) {
+ field.choices = choicesEl.value;
+ }
+
+ const defaultValueEl = document.getElementById('propDefaultValue');
+ if (defaultValueEl) {
+ field.default_value = defaultValueEl.value;
+ }
+
+ // Save min/max values for rating, slider, etc.
+ const minValEl = document.getElementById('propMinValue');
+ if (minValEl) {
+ if (!field.validation) field.validation = {};
+ field.validation.min_value = minValEl.value ? parseFloat(minValEl.value) : null;
+ }
+ const maxValEl = document.getElementById('propMaxValue');
+ if (maxValEl) {
+ if (!field.validation) field.validation = {};
+ field.validation.max_value = maxValEl.value ? parseFloat(maxValEl.value) : null;
+ }
+
+ // For matrix, try to parse choices as JSON
+ if (field.field_type === 'matrix' && choicesEl) {
+ try {
+ field.choices = JSON.parse(choicesEl.value);
+ } catch (e) {
+ // Keep as string if not valid JSON
+ }
+ }
+
+ // Save conditional logic
+ const enableConditional = document.getElementById('propEnableConditional');
+ if (enableConditional && enableConditional.checked) {
+ const conditions = [];
+ document.querySelectorAll('.condition-field').forEach((el, index) => {
+ const fieldName = el.value;
+ const operator = document.querySelector(`.condition-operator[data-index="${index}"]`).value;
+ const value = document.querySelector(`.condition-value[data-index="${index}"]`).value;
+
+ if (fieldName && operator) {
+ conditions.push({ field: fieldName, operator, value });
+ }
+ });
+
+ if (conditions.length > 0) {
+ field.conditional_rules = {
+ operator: document.getElementById('propConditionalOperator').value,
+ action: document.getElementById('propConditionalAction').value,
+ conditions: conditions
+ };
+ } else {
+ field.conditional_rules = null;
+ }
+
+ // Also check if JSON was edited directly
+ const jsonEl = document.getElementById('propConditionalRulesJson');
+ if (jsonEl && jsonEl.value.trim()) {
+ try {
+ field.conditional_rules = JSON.parse(jsonEl.value);
+ } catch (e) {
+ console.warn('Invalid conditional rules JSON, using UI values');
+ }
+ }
+ } else {
+ field.conditional_rules = null;
+ }
+
+ // Save validation rules
+ const validationRules = [];
+ document.querySelectorAll('.validation-type').forEach((el, index) => {
+ const type = el.value;
+ const value = document.querySelector(`.validation-value[data-index="${index}"]`)?.value;
+ const message = document.querySelector(`.validation-message[data-index="${index}"]`)?.value;
+
+ if (type) {
+ const rule = { type };
+ if (value) rule.value = value;
+ if (message) rule.message = message;
+ validationRules.push(rule);
+ }
+ });
+ field.validation_rules = validationRules.length > 0 ? validationRules : null;
+
+ // Also check if JSON was edited directly
+ const validationJsonEl = document.getElementById('propValidationRulesJson');
+ if (validationJsonEl && validationJsonEl.value.trim()) {
+ try {
+ field.validation_rules = JSON.parse(validationJsonEl.value);
+ } catch (e) {
+ console.warn('Invalid validation rules JSON, using UI values');
+ }
+ }
+
+ // Save field dependencies
+ const dependencies = [];
+ document.querySelectorAll('.dependency-source').forEach((el, index) => {
+ const sourceField = el.value;
+ const endpoint = document.querySelector(`.dependency-endpoint[data-index="${index}"]`)?.value;
+
+ if (sourceField && endpoint) {
+ dependencies.push({
+ sourceField: sourceField,
+ targetField: field.field_name,
+ apiEndpoint: endpoint
+ });
+ }
+ });
+ field.field_dependencies = dependencies.length > 0 ? dependencies : null;
+
+ // Also check if JSON was edited directly
+ const dependenciesJsonEl = document.getElementById('propDependenciesJson');
+ if (dependenciesJsonEl && dependenciesJsonEl.value.trim()) {
+ try {
+ field.field_dependencies = JSON.parse(dependenciesJsonEl.value);
+ } catch (e) {
+ console.warn('Invalid dependencies JSON, using UI values');
+ }
+ }
+
+ // Mark field as saved (no longer new)
+ this.isNewField = false;
+
+ // Close modal
+ bootstrap.Modal.getInstance(document.getElementById('fieldPropertyModal')).hide();
+
+ // Re-render
+ this.renderCanvas();
+ this.updatePreview();
+ },
+};
From 0c5f0754d4b0d8bc95b0185fd7eea3c1f943f508 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 3 Aug 2026 20:21:23 +0000
Subject: [PATCH 3/3] Fix duplicate data-index bugs in property editor add/save
handlers
Co-authored-by: matteius <479892+matteius@users.noreply.github.com>
---
.../js/form-builder-property-editor.js | 26 ++++++++++++++-----
1 file changed, 20 insertions(+), 6 deletions(-)
diff --git a/django_forms_workflows/static/django_forms_workflows/js/form-builder-property-editor.js b/django_forms_workflows/static/django_forms_workflows/js/form-builder-property-editor.js
index 37cb4a0..bda3b78 100644
--- a/django_forms_workflows/static/django_forms_workflows/js/form-builder-property-editor.js
+++ b/django_forms_workflows/static/django_forms_workflows/js/form-builder-property-editor.js
@@ -408,7 +408,10 @@ export const propertyEditorMethods = {
// Add event listener for add button
const btnAdd = document.getElementById('btnAddCondition');
if (btnAdd) {
- btnAdd.addEventListener('click', () => this.addConditionRow({}, conditions.length));
+ btnAdd.addEventListener('click', () => {
+ const nextIndex = container.querySelectorAll('.card').length;
+ this.addConditionRow({}, nextIndex);
+ });
}
},
@@ -471,7 +474,11 @@ export const propertyEditorMethods = {
// Add event listener for add button
const btnAdd = document.getElementById('btnAddValidation');
if (btnAdd) {
- btnAdd.addEventListener('click', () => this.addValidationRuleRow({}, rules.length));
+ btnAdd.addEventListener('click', () => {
+ const container = document.getElementById('validationRulesList');
+ const nextIndex = container ? container.querySelectorAll('.card').length : rules.length;
+ this.addValidationRuleRow({}, nextIndex);
+ });
}
},
@@ -527,7 +534,11 @@ export const propertyEditorMethods = {
// Add event listener for add button
const btnAdd = document.getElementById('btnAddDependency');
if (btnAdd) {
- btnAdd.addEventListener('click', () => this.addDependencyRow({}, dependencies.length));
+ btnAdd.addEventListener('click', () => {
+ const container = document.getElementById('dependenciesList');
+ const nextIndex = container ? container.querySelectorAll('.card').length : dependencies.length;
+ this.addDependencyRow({}, nextIndex);
+ });
}
},
@@ -629,7 +640,8 @@ export const propertyEditorMethods = {
const enableConditional = document.getElementById('propEnableConditional');
if (enableConditional && enableConditional.checked) {
const conditions = [];
- document.querySelectorAll('.condition-field').forEach((el, index) => {
+ document.querySelectorAll('.condition-field').forEach((el) => {
+ const index = el.dataset.index;
const fieldName = el.value;
const operator = document.querySelector(`.condition-operator[data-index="${index}"]`).value;
const value = document.querySelector(`.condition-value[data-index="${index}"]`).value;
@@ -664,7 +676,8 @@ export const propertyEditorMethods = {
// Save validation rules
const validationRules = [];
- document.querySelectorAll('.validation-type').forEach((el, index) => {
+ document.querySelectorAll('.validation-type').forEach((el) => {
+ const index = el.dataset.index;
const type = el.value;
const value = document.querySelector(`.validation-value[data-index="${index}"]`)?.value;
const message = document.querySelector(`.validation-message[data-index="${index}"]`)?.value;
@@ -690,7 +703,8 @@ export const propertyEditorMethods = {
// Save field dependencies
const dependencies = [];
- document.querySelectorAll('.dependency-source').forEach((el, index) => {
+ document.querySelectorAll('.dependency-source').forEach((el) => {
+ const index = el.dataset.index;
const sourceField = el.value;
const endpoint = document.querySelector(`.dependency-endpoint[data-index="${index}"]`)?.value;