Skip to content

Reject groups inside groups and group rules on their own questions in the API - #220

Merged
PhilReinking merged 4 commits into
mainfrom
fix/bits-179-group-checks
Oct 10, 2026
Merged

PhilReinking merged 4 commits into
mainfrom
fix/bits-179-group-checks

Conversation

@PhilReinking

@PhilReinking PhilReinking commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

The API and template import now check what the editor already prevents: no group inside a group, and no group rule on a question inside it. Before, such questions disappeared from the public form, or the rule sent respondents back to the start.

Fixes BITS-179

  • Sequence: a scope must be a top-level group of the same form, and a group gets none.
  • Another form's block id or a missing scope gives 422 before anything saves, not 500.
  • Logic rules: API and template import refuse a group rule whose condition uses a question inside that group.
  • Template import refuses a parent_block that isn't a top-level group of the template.
  • That import check is strict, not Rule::in, because Rule::in skips "", which the import also drops.

Heads-up: a form that already has a group inside a group can't be reordered in the editor until that group is moved to the top level by API.

Tested: full PHP suite. The 15 new test cases fail without the fix. The existing sequence, logic and template tests pass unchanged.
Not tested: dragging blocks in the editor by hand.

🤖 Generated with Claude Code

PhilReinking and others added 3 commits October 10, 2026 14:35
The sequence API only accepts blocks of the form and a top-level group
as scope, and a group keeps no scope. A rule on a group can't use a
question inside that group, by API and by template import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@PhilReinking PhilReinking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK at 6bebabd, CI green.

Checked beyond the PR text: the editor's real payload (every block, groups at the top level) saves, a mixed valid and invalid sequence saves nothing, and group rules on questions in another group still save. An old nested group blocks the editor, and one API call with that group's scope set to null fixes it. Two older gaps of the same kind are inline, fine as a follow-up too.

'sequence' => 'required|array',
'sequence.*.id' => ['required', Rule::in($form->formBlocks->pluck('id'))],
'sequence.*.scope' => [
'nullable',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A sequence item without a scope key still gives a 500, after the items before it are saved. With present it's a 422 before anything saves:

Suggested change
'nullable',
'present',
'nullable',

'blocks.*.id' => 'nullable|string',
'blocks.*.type' => ['required', Rule::enum(FormBlockType::class)],
// same as the sequence API, a group stays at the top level
'blocks.*.parent_block' => 'prohibited_if:blocks.*.type,'.FormBlockType::group->value,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A child whose parent_block points at a question or at a missing id is never created, and the import still answers 200. A group with parent_block: "" passes the new check, but the group and its questions are never created, after the form's old questions are deleted. Allowing only top-level groups of the template as parents catches all three:

Suggested change
'blocks.*.parent_block' => 'prohibited_if:blocks.*.type,'.FormBlockType::group->value,
'blocks.*.parent_block' => [
'nullable',
'prohibited_if:blocks.*.type,'.FormBlockType::group->value,
Rule::in(collect($template['blocks'] ?? [])->where('type', FormBlockType::group->value)->whereNull('parent_block')->pluck('id')->filter()),
],

I tried both suggestions together: the sequence, logic and template tests pass (69).

…level group

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@PhilReinking PhilReinking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK at 063a9ae, CI green.

The strict check is better than my Rule::in suggestion, since it also catches "" on a group without children. A child listed before its group still imports, and a numeric parent_block now gets a 422 instead of being dropped. The 5 new test cases fail without the change.

@PhilReinking
PhilReinking merged commit 01d92d7 into main Oct 10, 2026
4 checks passed
@PhilReinking
PhilReinking deleted the fix/bits-179-group-checks branch October 10, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant