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 => + `` + ).join(''); + + return ` +
+
+
+ + Conditional Logic allows you to show/hide or require/unrequire this field based on other field values. +
+
+ +
+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+ + + You can edit the JSON directly for advanced configurations +
+
+
+ `; + }, + + buildValidationTab(field) { + // Initialize validation_rules if not present + if (!field.validation_rules) { + field.validation_rules = []; + } + + const validationRulesJson = field.validation_rules.length > 0 ? JSON.stringify(field.validation_rules, null, 2) : ''; + + return ` +
+
+
+ + Validation Rules provide real-time client-side validation with custom error messages. +
+
+ +
+ +
+ +
+ +
+ + + You can edit the JSON directly for advanced configurations +
+
+ `; + }, + + buildDependenciesTab(field) { + // Initialize field_dependencies if not present + if (!field.field_dependencies) { + field.field_dependencies = []; + } + + const dependenciesJson = field.field_dependencies.length > 0 ? JSON.stringify(field.field_dependencies, 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 => + `` + ).join(''); + + return ` +
+
+
+ + Field Dependencies allow this field's options to update based on other field values (cascade updates). +
+
+ +
+ +
+ +
+ +
+ + + You can edit the JSON directly for advanced configurations +
+
+ `; + }, + + initializePropertyFormTabs(field) { + // Wires up the interactive bits of the Conditional Logic, Validation, + // and Dependencies tabs. + const enableConditional = document.getElementById('propEnableConditional'); + const conditionalRulesContainer = document.getElementById('conditionalRulesContainer'); + if (enableConditional && conditionalRulesContainer) { + enableConditional.addEventListener('change', (e) => { + conditionalRulesContainer.style.display = e.target.checked ? 'block' : 'none'; + }); + } + this.initializeConditionsList(field.conditional_rules?.conditions || []); + this.initializeValidationRulesList(field.validation_rules || []); + this.initializeDependenciesList(field.field_dependencies || []); + }, + + initializeConditionsList(conditions) { + const container = document.getElementById('conditionsList'); + if (!container) return; + + container.innerHTML = ''; + conditions.forEach((condition, index) => { + this.addConditionRow(condition, index); + }); + + // Add event listener for add button + const btnAdd = document.getElementById('btnAddCondition'); + if (btnAdd) { + btnAdd.addEventListener('click', () => this.addConditionRow({}, conditions.length)); + } + }, + + addConditionRow(condition, index) { + const container = document.getElementById('conditionsList'); + if (!container) return; + + const otherFields = this.fields.filter(f => f.field_name !== this.fields[this.currentFieldIndex].field_name); + const fieldOptions = otherFields.map(f => + `` + ).join(''); + + const row = document.createElement('div'); + row.className = 'card mb-2'; + row.innerHTML = ` +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ `; + 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 => + `` + ).join(''); + + const row = document.createElement('div'); + row.className = 'card mb-2'; + row.innerHTML = ` +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ `; + 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;