Skip to content

48849 frontend [ fieldsets ] Advanced fieldset rules - #344

Open
Maria-Lordwill wants to merge 48 commits into
masterfrom
frontend/fieldsets/48849__advanced_fieldset_rules
Open

48849 frontend [ fieldsets ] Advanced fieldset rules#344
Maria-Lordwill wants to merge 48 commits into
masterfrom
frontend/fieldsets/48849__advanced_fieldset_rules

Conversation

@Maria-Lordwill

@Maria-Lordwill Maria-Lordwill commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Note

High Risk
Breaking API and schema change for fieldset rules, plus a data migration that rewrites existing rule rows. Incomplete wiring of field-level rulesets and leftover rules vs rulesets usage in workflow version updates could drop or mis-apply rules at runtime.

Overview
Replaces fieldset rules with nested rulesets (OR groups of AND conditions) so users can combine sum comparisons (sum_equal, sum_greater_than, sum_less_than) instead of a single flat sum_equal rule.

Backend adds template and workflow models for fieldset and field-level rulesets (FieldSet*RuleSet / Field*RuleSet plus GroupOr/GroupAnd). Old FieldsetTemplateRule / FieldSetRule stay but are marked deprecated. A data migration copies existing fieldset template rules into the new hierarchy. Template APIs, fieldset services, clone/sync, and workflow instantiation now persist and validate rulesets. Unique constraints on field/fieldset templates now include account.

Frontend fieldset editor uses a new FieldsetRulesets UI (add/delete rulesets, AND/OR regrouping, field picker, custom messages) and validators for nested sum rules. Payload field is rulesets throughout.

Breaking: fieldset request/response no longer uses rules. Field-level show/validate rulesets are modeled and serialized on field templates, but fieldset field creation still drops rulesets for now.

Reviewed by Cursor Bugbot for commit 00c9dd7. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Replace flat fieldset rules with hierarchical rulesets (nested OR/AND groups)

  • Introduces new template and workflow models for FieldSetTemplateRuleSetFieldSetTemplateRuleGroupOrFieldSetTemplateRuleGroupAnd, plus equivalent field-level ruleset models, each with operator/value semantics instead of a flat rule type/value pair
  • Adds new enums FieldRuleType (SHOW, VALIDATOR), FieldRuleOperator (EQUAL, GREATER_THAN, LESS_THAN), and extends FieldSetRuleOperator with SUM_GREATER_THAN and SUM_LESS_THAN
  • Reworks serializers and services (FieldSetTemplateService, FieldsetTemplateRuleSetService, TaskUpdateVersionService, KickoffUpdateVersionService, TaskFieldService) to create, upsert by api_name, and prune nested rulesets with validation for SUM operators at the AND-group level
  • Ships a data migration (0260_auto_20260814_2003.py) that converts legacy FieldsetTemplateRule rows into the new ruleset/group/group-and structure and reassigns M2M fields; duplicate (fieldset_id, api_name) rows are skipped
  • Rebuilds the frontend FieldsetDetails page with a new FieldsetRulesets component tree (cards, rule items, field selectors), updates types, validators, factories, and i18n strings to the rulesets model
  • Risk: the API contract for fieldset and field rule definitions changes from rules to rulesets with nested groups_or/groups_and; any out-of-tree consumers sending the old rules payload will break. Legacy FieldsetTemplateRule and FieldSetRule models remain but are deprecated. Unique constraints on FieldTemplate and FieldsetTemplate now include account, which may affect existing cross-account data

Macroscope summarized 00c9dd7.

Maria-Lordwill and others added 30 commits August 7, 2026 19:59
…to frontend/fieldsets/48741__provide_information_regarding_the_binding_of_the_field_set_to_specific_templates
…to frontend/fieldsets/48741__provide_information_regarding_the_binding_of_the_field_set_to_specific_templates
…ent field in run/kickoff, truncate large text placeholder in run
Maria-Lordwill and others added 5 commits August 17, 2026 20:00
…to frontend/fieldsets/48741__provide_information_regarding_the_binding_of_the_field_set_to_specific_templates
…esets_2' into frontend/fieldsets/48849__advanced_fieldset_rules

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High

service.partial_update(force_save=True, **field_data)

Updating a fieldset with an existing field raises AttributeError instead of saving because FieldSetTemplateService._update_fields forwards rulesets to FieldTemplateService.partial_update, which attempts setattr(field, 'rulesets', value) on the reverse relation. Remove rulesets before the call, or handle it through the field-ruleset service.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @backend/src/processes/services/fieldsets/fieldset.py around line 238:

Updating a fieldset with an existing field raises `AttributeError` instead of saving because `FieldSetTemplateService._update_fields` forwards `rulesets` to `FieldTemplateService.partial_update`, which attempts `setattr(field, 'rulesets', value)` on the reverse relation. Remove `rulesets` before the call, or handle it through the field-ruleset service.

Comment thread backend/src/processes/serializers/templates/field_rule.py
)
service.partial_update(**rule_data)
rule_api_names.add(rule_api_name)
service.partial_update(**ruleset_data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium fieldsets/fieldset.py:403

Updating an existing ruleset with a new fields list persists text fields in a sum rule without revalidating its unchanged groups_or, so invalid numeric validation behavior is accepted. FieldsetTemplateRuleSetService.partial_update only validates groups when they are created or updated; revalidate the existing sum groups after updating fields.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @backend/src/processes/services/fieldsets/fieldset.py around line 403:

Updating an existing ruleset with a new `fields` list persists text fields in a sum rule without revalidating its unchanged `groups_or`, so invalid numeric validation behavior is accepted. `FieldsetTemplateRuleSetService.partial_update` only validates groups when they are created or updated; revalidate the existing sum groups after updating `fields`.


def validate_rules(self) -> bool:
rules = list(self.instance.rules.order_by('type').all())
rules = list(self.instance.rulesets.order_by('type').all())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High fieldsets/fieldset.py:83

validate_rules raises Django FieldError before validating any rules because FieldSetRuleSet has no type field for order_by('type'); the rule operators are stored on nested groups_and records. Update the query and grouping logic to use the nested operator field.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @backend/src/processes/services/workflows/fieldsets/fieldset.py around line 83:

`validate_rules` raises Django `FieldError` before validating any rules because `FieldSetRuleSet` has no `type` field for `order_by('type')`; the rule operators are stored on nested `groups_and` records. Update the query and grouping logic to use the nested operator field.

)
rule_ids.append(rule.id)
fieldset.rules.exclude(id__in=rule_ids).delete()
fieldset.rulesets.exclude(id__in=rule_ids).delete()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High workflows/kickoff_version.py:119

Updating a kickoff version can delete unrelated FieldSetRuleSet records and leave the requested rules only in deprecated FieldSetRule rows. _update_fieldset_rules collects IDs from FieldSetRule.objects but applies them to fieldset.rulesets, so the new relation is never populated and its records are removed when their IDs are absent. Create/update the new ruleset model and use those records for this cleanup.

Also found in 1 other location(s)

backend/src/processes/services/tasks/task_version.py:236

_update_fieldset_rules still creates/updates legacy FieldSetRule rows, but line 236 treats their IDs as IDs in the new FieldSetRuleSet reverse relation. Updating a task version can therefore delete unrelated new rulesets whose IDs are absent from (or collide with) the legacy IDs, while never creating/updating the requested new rulesets.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @backend/src/processes/services/workflows/kickoff_version.py around line 119:

Updating a kickoff version can delete unrelated `FieldSetRuleSet` records and leave the requested rules only in deprecated `FieldSetRule` rows. `_update_fieldset_rules` collects IDs from `FieldSetRule.objects` but applies them to `fieldset.rulesets`, so the new relation is never populated and its records are removed when their IDs are absent. Create/update the new ruleset model and use those records for this cleanup.

Also found in 1 other location(s):
- backend/src/processes/services/tasks/task_version.py:236 -- `_update_fieldset_rules` still creates/updates legacy `FieldSetRule` rows, but line 236 treats their IDs as IDs in the new `FieldSetRuleSet` reverse relation. Updating a task version can therefore delete unrelated new rulesets whose IDs are absent from (or collide with) the legacy IDs, while never creating/updating the requested new rulesets.

+ {formatMessage({ id: 'fieldsets.add-rule' })}
</button>
</div>
<FieldsetRulesets

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium FieldsetDetails/FieldsetDetails.tsx:373

Deleting the only condition leaves an empty, undeletable ruleset, so validateFieldsetRules rejects the changes and the user cannot save or recover without discarding the entire edit. FieldsetRulesets always calls deleteGroupAnd, which removes the condition but leaves the parent ruleset; use deleteRuleset for the final condition or provide a way to add a new condition.

Also found in 2 other location(s)

frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetRulesets/FieldsetRulesets.tsx:84

The delete button always calls deleteGroupAnd, even when this is the ruleset's only condition. Deleting that condition leaves the ruleset with an empty groupsOr; this component provides no way to delete that ruleset or add a condition back, and validateFieldsetRules rejects it. The user is therefore stuck with an unsaveable ruleset and must reload, losing other edits. Use deleteRuleset for the final condition or expose controls to recover the empty ruleset.

frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetRulesets/utils.ts:122

deleteGroupAnd removes the containing groupOr when its last condition is deleted, but leaves the parent ruleset in place. In the supplied UI this delete button is also used for the sole condition created by createEmptyRuleset, and there is no control that calls addGroupAnd or deleteRuleset; after clicking it, the ruleset has groupsOr: [], renders no condition inputs, and cannot be repaired or removed. The user is left with an invalid, undeletable ruleset that prevents saving until the entire edit is discarded.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.tsx around line 373:

Deleting the only condition leaves an empty, undeletable ruleset, so `validateFieldsetRules` rejects the changes and the user cannot save or recover without discarding the entire edit. `FieldsetRulesets` always calls `deleteGroupAnd`, which removes the condition but leaves the parent ruleset; use `deleteRuleset` for the final condition or provide a way to add a new condition.

Also found in 2 other location(s):
- frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetRulesets/FieldsetRulesets.tsx:84 -- The delete button always calls `deleteGroupAnd`, even when this is the ruleset's only condition. Deleting that condition leaves the ruleset with an empty `groupsOr`; this component provides no way to delete that ruleset or add a condition back, and `validateFieldsetRules` rejects it. The user is therefore stuck with an unsaveable ruleset and must reload, losing other edits. Use `deleteRuleset` for the final condition or expose controls to recover the empty ruleset.
- frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetRulesets/utils.ts:122 -- `deleteGroupAnd` removes the containing `groupOr` when its last condition is deleted, but leaves the parent ruleset in place. In the supplied UI this delete button is also used for the sole condition created by `createEmptyRuleset`, and there is no control that calls `addGroupAnd` or `deleteRuleset`; after clicking it, the ruleset has `groupsOr: []`, renders no condition inputs, and cannot be repaired or removed. The user is left with an invalid, undeletable ruleset that prevents saving until the entire edit is discarded.

for rule_template in ruleset_templates:
service = FieldSetRuleService(user=self.user)
service.create(
instance_template=rule_template,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High fieldsets/fieldset.py:73

Creating a workflow from a template containing a new FieldSetTemplateRuleSet raises AttributeError instead of cloning its ruleset. _create_rules passes the new ruleset to the legacy FieldSetRuleService, which expects type and value fields that the nested-group ruleset does not provide; use the ruleset-aware cloning service instead.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @backend/src/processes/services/workflows/fieldsets/fieldset.py around line 73:

Creating a workflow from a template containing a new `FieldSetTemplateRuleSet` raises `AttributeError` instead of cloning its ruleset. `_create_rules` passes the new ruleset to the legacy `FieldSetRuleService`, which expects `type` and `value` fields that the nested-group ruleset does not provide; use the ruleset-aware cloning service instead.

field = models.ForeignKey(
TaskField,
on_delete=models.CASCADE,
related_name='rulesets',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High workflows/fields.py:125

Versioning a field with legacy rules raises a Django model-type TypeError and aborts workflow creation. TaskField.rulesets is the reverse-FK manager for FieldRuleSet, but _link_rules passes it FieldSetRule instances; use the legacy TaskField.fieldset_rulesets relation (or otherwise convert the objects) when linking those rules.

Also found in 3 other location(s)

backend/src/processes/services/tasks/field.py:399

_link_rules still queries deprecated FieldSetRule objects, but now passes them to self.instance.rulesets.set(...). TaskField.rulesets is the reverse foreign-key manager for FieldRuleSet.field, so it cannot accept FieldSetRule instances. Creating a task field whose template has any ruleset reaches this path and raises a model-type error, aborting workflow/task creation. The fieldset ruleset relation is instead exposed through TaskField.fieldset_rulesets.

backend/src/processes/services/tasks/task_version.py:252

rules is a queryset of legacy FieldSetRule objects, but field.rulesets is the reverse FK manager for new FieldRuleSet objects. Calling field.rulesets.set(rules) with the legacy model instances raises a relation type/value error whenever a versioned field has rules, so task version updates fail instead of linking rules.

backend/src/processes/services/workflows/kickoff_version.py:135

The queryset assigned at line 135 contains legacy FieldSetRule instances, whereas field.rulesets manages new FieldRuleSet instances. For any kickoff field with nonempty rules, .set(rules) rejects the wrong model type and aborts the kickoff version update.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @backend/src/processes/models/workflows/fields.py around line 125:

Versioning a field with legacy rules raises a Django model-type `TypeError` and aborts workflow creation. `TaskField.rulesets` is the reverse-FK manager for `FieldRuleSet`, but `_link_rules` passes it `FieldSetRule` instances; use the legacy `TaskField.fieldset_rulesets` relation (or otherwise convert the objects) when linking those rules.

Also found in 3 other location(s):
- backend/src/processes/services/tasks/field.py:399 -- `_link_rules` still queries deprecated `FieldSetRule` objects, but now passes them to `self.instance.rulesets.set(...)`. `TaskField.rulesets` is the reverse foreign-key manager for `FieldRuleSet.field`, so it cannot accept `FieldSetRule` instances. Creating a task field whose template has any ruleset reaches this path and raises a model-type error, aborting workflow/task creation. The fieldset ruleset relation is instead exposed through `TaskField.fieldset_rulesets`.
- backend/src/processes/services/tasks/task_version.py:252 -- `rules` is a queryset of legacy `FieldSetRule` objects, but `field.rulesets` is the reverse FK manager for new `FieldRuleSet` objects. Calling `field.rulesets.set(rules)` with the legacy model instances raises a relation type/value error whenever a versioned field has rules, so task version updates fail instead of linking rules.
- backend/src/processes/services/workflows/kickoff_version.py:135 -- The queryset assigned at line 135 contains legacy `FieldSetRule` instances, whereas `field.rulesets` manages new `FieldRuleSet` instances. For any kickoff field with nonempty rules, `.set(rules)` rejects the wrong model type and aborts the kickoff version update.

Comment thread backend/src/processes/services/fieldsets/fieldset_rule.py
Comment thread backend/src/processes/services/workflows/fieldsets/fieldset.py
Comment thread backend/src/processes/services/tasks/field.py
Comment thread backend/src/processes/services/fieldsets/fieldset.py
Comment thread backend/src/processes/services/fieldsets/fieldset.py
<div className={kickoffStyles['kick-off-input__dropdown']}>
<FieldsetFlowRowDropdown
headerTitle={title}
headerTitle={apiNameBinding}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropdown header shows internal API name

Medium Severity

The fieldset row menu now passes apiNameBinding into headerTitle, which is rendered as the dropdown header. Users see the internal binding id instead of the fieldset title that was shown before.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5ca2c38. Configure here.

<InfiniteScroll
dataLength={fieldsetsList.length}
next={() => dispatch(loadFieldsets({ offset: offset + 1 }))}
next={() => !isLoading && dispatch(loadFieldsets({ offset: offset + 1 }))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Fieldsets/Fieldsets.tsx:44

Pagination can stop permanently after a sorting reload: InfiniteScroll invokes next while the reload is in progress, line 44 returns without dispatching loadFieldsets, and dataLength may not change when the first sorted page replaces the previous page. Because the scroll component only rearms after a dataLength change, later pages are never requested. Remove this guard (or make hasMore false while loading) so an invoked next is not recorded without loading the next page.

Suggested change
next={() => !isLoading && dispatch(loadFieldsets({ offset: offset + 1 }))}
next={() => dispatch(loadFieldsets({ offset: offset + 1 }))}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @frontend/src/public/components/Fieldsets/Fieldsets.tsx around line 44:

Pagination can stop permanently after a sorting reload: `InfiniteScroll` invokes `next` while the reload is in progress, line 44 returns without dispatching `loadFieldsets`, and `dataLength` may not change when the first sorted page replaces the previous page. Because the scroll component only rearms after a `dataLength` change, later pages are never requested. Remove this guard (or make `hasMore` false while loading) so an invoked `next` is not recorded without loading the next page.

'fieldsets.leave-unsaved-message': 'Ваши изменения не сохранены.',
'fieldsets.leave-unsaved-stay': 'Продолжить редактирование',
'fieldsets.leave-unsaved-leave': 'Отменить',
'fieldsets.leave-unsaved-leave': 'Убрать',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium locales/ru_RU.ts:1435

The fieldsets.leave-unsaved-leave button is labeled Убрать (“Remove”), so Russian users are not told that confirmLeave will discard their unsaved edits. Use a discard-specific translation such as Отменить изменения or Не сохранять, matching the English Discard action.

Suggested change
'fieldsets.leave-unsaved-leave': 'Убрать',
'fieldsets.leave-unsaved-leave': 'Отменить изменения',
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @frontend/src/public/lang/locales/ru_RU.ts around line 1435:

The `fieldsets.leave-unsaved-leave` button is labeled `Убрать` (“Remove”), so Russian users are not told that `confirmLeave` will discard their unsaved edits. Use a discard-specific translation such as `Отменить изменения` or `Не сохранять`, matching the English `Discard` action.

color: var(--pneumatic-color-notification1);
background: transparent;
border: 0;
opacity: 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium FieldsetDetails/FieldsetDetails.css:485

Keyboard users can focus the real delete button while .rule-remove-btn remains invisible, because visibility currently depends only on .rule-row:hover or (hover: none). Add a :focus-visible rule so the focused destructive control is visible.

-  opacity: 0;
+  opacity: 0;
+
+  &:focus-visible {
+    opacity: 1;
+  }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @frontend/src/public/components/Fieldsets/FieldsetDetails/FieldsetDetails.css around line 485:

Keyboard users can focus the real delete button while `.rule-remove-btn` remains invisible, because visibility currently depends only on `.rule-row:hover` or `(hover: none)`. Add a `:focus-visible` rule so the focused destructive control is visible.

field.rulesets.set(rules)
else:
field.rules.clear()
field.rulesets.clear()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version update keeps flat rules API

High Severity

_update_fieldset_rules still update_or_creates FieldSetRule from flat type/value, then deletes via fieldset.rulesets. Callers still pass fieldset_data.get('rules'), and _update_field_rules still reads rules and assigns FieldSetRule rows onto field.rulesets. Template version sync drops or corrupts nested rulesets.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9a04fcb. Configure here.

Comment thread backend/src/processes/tests/fixtures.py Outdated
type=shared_rule.type,
value=shared_rule.value,
api_name=f'{fieldset.api_name}-ruleset-1',
type=shared_ruleset.type,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixture creates ruleset with type

Medium Severity

create_test_fieldset_template creates FieldSetTemplateRuleSet with type=shared_ruleset.type, but that model has no type. __str__ on both template and workflow ruleset models also interpolates self.type, which will raise if invoked.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9a04fcb. Configure here.

Comment thread backend/src/processes/services/fieldsets/fieldset_rule.py
Comment thread backend/src/processes/serializers/templates/fieldset_rule.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 80e88cf. Configure here.

&:hover .rule-remove-btn {
opacity: 1;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule borders always removed

Low Severity

The new .rule-row block adds :last-child { border-bottom: none }, but each .rule-row sits alone as the last child inside its own .rule-item, so every row matches and loses its separator. The block also duplicates the existing .rule-row definition instead of extending it.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 80e88cf. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Frontend Web client changes request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants