From 9ab22c36fcf52e2afa6ac4b95ee556ec22900593 Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Wed, 4 Nov 2026 12:50:43 +0100 Subject: [PATCH 1/2] feat(conditional): add checkboxes as trigger A checkbox trigger activates a branch when all options of one of its option combinations are selected, so several branches can be shown at once. The condition editor picks the combination, copies of a question map the option ids of the combinations to the copied options, and the extra settings only accept integer option ids. The code comes from the original pull request #3123 by Micke Nordin and TimedIn, with the review fixes applied. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Andy Scherzinger --- docs/DataStructure.md | 9 +++ lib/Constants.php | 2 + lib/Service/FormsService.php | 7 ++ lib/Service/SubmissionService.php | 12 +++ .../Questions/BranchConditionEditor.vue | 55 +++++++++++-- .../Questions/QuestionConditional.vue | 31 ++++++- tests/Unit/Service/FormsServiceTest.php | 20 +++++ tests/Unit/Service/SubmissionServiceTest.php | 80 +++++++++++++++++++ 8 files changed, 206 insertions(+), 10 deletions(-) diff --git a/docs/DataStructure.md b/docs/DataStructure.md index b64fcbcb4..3c0a0257f 100644 --- a/docs/DataStructure.md +++ b/docs/DataStructure.md @@ -307,6 +307,7 @@ Supported trigger types for conditional questions: | ----------------- | --------------------------------------------------------------------------- | ------------------------------------------------- | | `multiple_unique` | `option_selected` | Radio buttons - the selected option | | `dropdown` | `option_selected` | Dropdown - the selected option | +| `multiple` | `options_combination` | Checkboxes - all options of a combination | A branch is shown when any of its conditions matches. Several branches can be shown at once. @@ -348,6 +349,14 @@ Conditions determine when a branch is activated. The structure depends on the tr { "type": "option_selected", "optionId": 42 } ``` +#### options_combination (for multiple) + +```json +{ "type": "options_combination", "optionIds": [42, 43] } +``` + +All options in `optionIds` must be selected for the condition to match. + ### Conditional Question Example A complete conditional question structure: diff --git a/lib/Constants.php b/lib/Constants.php index 8058242ce..bd7ee9c77 100644 --- a/lib/Constants.php +++ b/lib/Constants.php @@ -260,6 +260,7 @@ class Constants { * Condition types for conditional questions */ public const CONDITION_TYPE_OPTION_SELECTED = 'option_selected'; + public const CONDITION_TYPE_OPTIONS_COMBINATION = 'options_combination'; /** * Trigger types allowed for conditional questions @@ -268,6 +269,7 @@ class Constants { public const CONDITIONAL_TRIGGER_TYPES = [ self::ANSWER_TYPE_MULTIPLEUNIQUE => [self::CONDITION_TYPE_OPTION_SELECTED], self::ANSWER_TYPE_DROPDOWN => [self::CONDITION_TYPE_OPTION_SELECTED], + self::ANSWER_TYPE_MULTIPLE => [self::CONDITION_TYPE_OPTIONS_COMBINATION], ]; /** diff --git a/lib/Service/FormsService.php b/lib/Service/FormsService.php index bc6666562..863aa132c 100644 --- a/lib/Service/FormsService.php +++ b/lib/Service/FormsService.php @@ -156,6 +156,9 @@ public static function mapConditionOptionIds(array $extraSettings, array $option if (isset($condition['optionId'])) { $extraSettings['branches'][$branchIndex]['conditions'][$conditionIndex]['optionId'] = $optionIdMap[$condition['optionId']] ?? $condition['optionId']; } + if (isset($condition['optionIds']) && is_array($condition['optionIds'])) { + $extraSettings['branches'][$branchIndex]['conditions'][$conditionIndex]['optionIds'] = array_map(static fn ($optionId) => $optionIdMap[$optionId] ?? $optionId, $condition['optionIds']); + } } } return $extraSettings; @@ -953,6 +956,10 @@ private function areBranchConditionsValid(string $triggerType, array $conditions if (isset($condition['optionId']) && !is_int($condition['optionId'])) { return false; } + if (isset($condition['optionIds']) + && (!is_array($condition['optionIds']) || array_filter($condition['optionIds'], static fn ($id) => !is_int($id)) !== [])) { + return false; + } foreach (['value', 'min', 'max'] as $key) { if (isset($condition[$key]) && !is_scalar($condition[$key])) { return false; diff --git a/lib/Service/SubmissionService.php b/lib/Service/SubmissionService.php index 719f7582e..f4fa13358 100644 --- a/lib/Service/SubmissionService.php +++ b/lib/Service/SubmissionService.php @@ -1147,6 +1147,18 @@ private function evaluateBranchConditions(string $triggerType, array $triggerAns } } return false; + case Constants::ANSWER_TYPE_MULTIPLE: + // Multi-select: the branch is active if all option ids of any of its conditions are selected + foreach ($conditions as $condition) { + $optionIds = $condition['optionIds'] ?? []; + if (empty($optionIds) || !is_array($optionIds)) { + continue; + } + if (array_diff(array_map('strval', $optionIds), $selectedOptionIds) === []) { + return true; + } + } + return false; default: return false; } diff --git a/src/components/Questions/BranchConditionEditor.vue b/src/components/Questions/BranchConditionEditor.vue index 0f8daf50f..eb74f74b0 100644 --- a/src/components/Questions/BranchConditionEditor.vue +++ b/src/components/Questions/BranchConditionEditor.vue @@ -5,10 +5,12 @@