Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0096a5d
Feat: Conditional Logic
mickenordin Jan 18, 2026
dc9bf99
fix: allow empty branches, fixes
timedin-de May 18, 2026
53969d4
fix: trigger question options
timedin-de May 9, 2026
4c5e651
fix: show conditional answers in detail view
timedin-de May 18, 2026
3a0d42c
fix: save options and extraSettings for subquestions
timedin-de May 10, 2026
3be7668
fix: move up/down for subquestions
timedin-de May 10, 2026
dc3dce2
feat: add ranking-question to conditional
timedin-de May 29, 2026
7fe2f70
fix: multiple active branches
timedin-de May 29, 2026
56ca86c
fix: selectors for linear scale
timedin-de Jun 2, 2026
664edda
fix: extract question list to seperate component, add 'insert questio…
timedin-de Jun 2, 2026
3bb2dd5
feat: adding transition to conditional branches
timedin-de Jun 2, 2026
803d7e3
fix: harden conditional submission handling
mickenordin Jun 3, 2026
ae80c43
fix: selectors for linear scale
timedin-de Jun 2, 2026
8431f7b
fix: use constants instead of raw strings
timedin-de Jun 4, 2026
49982e8
fix: cloning forms
timedin-de Jun 6, 2026
d3a0fc0
fix: tests
timedin-de Jun 6, 2026
81acec6
fix: backend support for multiple active branches
timedin-de Jun 6, 2026
bc129fd
fix: conditional branch test mock and psalm types
mickenordin Jun 7, 2026
be331ea
fix: align conditional question view types
mickenordin Sep 15, 2026
c751406
test: cover upload authorization for conditional answers
mickenordin Sep 15, 2026
021429d
fix: use valid template syntax for answer updates
mickenordin Oct 8, 2026
0fc43fd
fix: avoid persisting conditional subquestions twice
mickenordin Oct 8, 2026
7149fe6
fix: simplify conditional trigger type selection
mickenordin Oct 8, 2026
e5e3cdd
fix(conditional): keep subquestions in their place on question updates
AndyScherzinger Nov 4, 2026
9e00375
fix(conditional): keep branches, order and copies of subquestions con…
AndyScherzinger Nov 4, 2026
bfe6a64
fix(conditional): validate and store trigger answers like regular que…
AndyScherzinger Nov 4, 2026
ed43068
fix(conditional): number and label subquestions apart from other ques…
AndyScherzinger Nov 4, 2026
44379c7
fix(conditional): match conditions the same way in the browser and on…
AndyScherzinger Nov 4, 2026
ed32b7e
fix(conditional): summarize subquestions and keep the trigger menu to…
AndyScherzinger Nov 4, 2026
8a69cc8
test(e2e): cover answering a conditional question
AndyScherzinger Nov 4, 2026
82c43c8
chore(conditional): name the migration after the 5.5 release
AndyScherzinger Nov 4, 2026
f5fbdc7
fix(conditional): indent subquestions less on phones
AndyScherzinger Nov 4, 2026
33fef4f
docs(conditional): list the condition types the server accepts
AndyScherzinger Nov 4, 2026
0cff919
feat(conditional): start with radio button and dropdown triggers
AndyScherzinger Nov 4, 2026
6c6cc98
test(conditional): cover reordering subquestions and loading them
AndyScherzinger Nov 4, 2026
d2dbfac
fix(submission): only accept question and option ids as they are
AndyScherzinger Nov 4, 2026
60bcfca
fix(migration): skip answers of deleted questions on import
AndyScherzinger Nov 4, 2026
21f29c0
fix(a11y): focus the title of new questions of every type
AndyScherzinger Nov 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 125 additions & 1 deletion docs/DataStructure.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!--
- SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
- SPDX-FileCopyrightText: 2021-2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-only
-->

Expand Down Expand Up @@ -240,6 +240,7 @@ Currently supported Question-Types are:
| `linearscale` | A linear or Likert scale question where you choose an option that best fits your opinion |
| `color` | A color answer, hex string representation (e. g. `#123456`) |
| `ranking` | Using pre-defined options, the user ranks them from most to least preferred. Needs at least one option available. Answers are stored in ranked order (one answer row per option). |
| `conditional` | A conditional branching question with a trigger question and multiple branches containing subquestions |

## Extra Settings

Expand Down Expand Up @@ -282,3 +283,126 @@ Supported option types and their intended usage.
| `column` | Represents a column header when rendering options in a grid-style question. |

`row` and `column` option types are primarily used together with `grid` question types to build a two-dimensional selection matrix. `choice` is the default for normal option lists.
| `triggerType` | `conditional` | string | [See trigger types](#conditional-trigger-types) | The type of trigger question (dropdown, multiple_unique, etc.) |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Something seems to be wrong with this addition here, probably misplaced after a rebase?

| `branches` | `conditional` | Array | Array of [Branch objects](#branch-object) | The branches with conditions and subquestions |

## Conditional Questions

Conditional questions enable branching logic in forms. A trigger question determines which branch of subquestions appears based on the respondent's answer.

### Question Properties for Subquestions

Subquestions (questions belonging to a conditional question's branch) have additional properties:

| Property | Type | Description |
| ---------------- | ------- | ---------------------------------------------------------------------- |
| parentQuestionId | Integer | The ID of the parent conditional question (null for regular questions) |
| branchId | String | The ID of the branch this subquestion belongs to |

### Conditional Trigger Types

Supported trigger types for conditional questions:

| Trigger Type | Condition Types | Description |
| ----------------- | --------------------------------------------------------------------------- | ------------------------------------------------- |
| `multiple_unique` | `option_selected` | Radio buttons - the selected option |
| `dropdown` | `option_selected` | Dropdown - the selected option |

A branch is shown when any of its conditions matches. Several branches can be shown at once.

### Branch Object

A branch defines conditions and subquestions that appear when those conditions are met.

| Property | Type | Description |
| ------------ | ---------------------------------------- | ---------------------------------------------- |
| id | String | Unique identifier for the branch |
| conditions | Array of [Conditions](#condition-object) | Conditions that must be met to show the branch |
| subQuestions | Array of [Questions](#question) | Questions shown when conditions are met |

```json
{
"id": "branch-1705587600000",
"conditions": [{ "type": "option_selected", "optionId": 42 }],
"subQuestions": [
{
"id": 101,
"formId": 3,
"order": 1,
"type": "short",
"text": "Please provide details",
"parentQuestionId": 100,
"branchId": "branch-1705587600000"
}
]
}
```

### Condition Object

Conditions determine when a branch is activated. The structure depends on the trigger type.

#### option_selected (for dropdown, multiple_unique)

```json
{ "type": "option_selected", "optionId": 42 }
```

### Conditional Question Example

A complete conditional question structure:

```json
{
"id": 100,
"formId": 3,
"order": 1,
"type": "conditional",
"isRequired": true,
"text": "Do you have any dietary restrictions?",
"options": [
{ "id": 42, "questionId": 100, "order": 1, "text": "Yes" },
{ "id": 43, "questionId": 100, "order": 2, "text": "No" }
],
"extraSettings": {
"triggerType": "dropdown",
"branches": [
{
"id": "branch-yes",
"conditions": [{ "type": "option_selected", "optionId": 42 }],
"subQuestions": [
{
"id": 101,
"formId": 3,
"order": 1,
"type": "long",
"text": "Please describe your dietary restrictions",
"parentQuestionId": 100,
"branchId": "branch-yes"
}
]
}
]
}
}
```

### Conditional Answer Structure

When submitting or storing conditional question answers, the structure differs from regular questions:

```json
{
"100": {
"trigger": ["42"],
"subQuestions": {
"101": ["Vegetarian, no nuts"]
}
}
}
```

| Property | Type | Description |
| ------------ | --------------------------- | --------------------------------------------------- |
| trigger | Array of strings | Answer values for the trigger question |
| subQuestions | Object (questionId → Array) | Map of subquestion IDs to their answer value arrays |
4 changes: 2 additions & 2 deletions lib/Analytics/AnalyticsDatasource.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ public function readData($option): array {

$formId = $option['formId'];

$questions = $this->formsService->getQuestions($formId);
foreach ($questions as $question) {
// Answers of subquestions are counted with the text of their subquestion
foreach (FormsService::flattenQuestions($this->formsService->getQuestions($formId)) as $question) {
$questionMap[$question['id']] = $question['text'];
}

Expand Down
33 changes: 32 additions & 1 deletion lib/Constants.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php

/**
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2021-2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

Expand Down Expand Up @@ -101,6 +101,7 @@ class Constants {

// Available AnswerTypes
public const ANSWER_TYPE_COLOR = 'color';
public const ANSWER_TYPE_CONDITIONAL = 'conditional';
public const ANSWER_TYPE_DATE = 'date';
public const ANSWER_TYPE_DATETIME = 'datetime';
public const ANSWER_TYPE_DROPDOWN = 'dropdown';
Expand All @@ -121,6 +122,7 @@ class Constants {
// All AnswerTypes
public const ANSWER_TYPES = [
self::ANSWER_TYPE_COLOR,
self::ANSWER_TYPE_CONDITIONAL,
self::ANSWER_TYPE_DATE,
self::ANSWER_TYPE_DATETIME,
self::ANSWER_TYPE_DROPDOWN,
Expand Down Expand Up @@ -239,6 +241,35 @@ class Constants {
self::ANSWER_GRID_TYPE_RADIO,
];

/**
* Extra settings for conditional questions
* - triggerType: The question type used for the trigger (e.g., 'multiple_unique', 'dropdown', 'short')
* - branches: Array of branch definitions, each containing:
* - id: Unique branch identifier
* - conditions: Array of condition objects defining when this branch is active
* For predefined types: [{ optionId: number }]
* For text types: [{ type: 'string_equals'|'string_contains'|'regex', value: string }]
* For numeric/scale: [{ type: 'value_equals'|'value_range', value: number, min?: number, max?: number }]
*/
public const EXTRA_SETTINGS_CONDITIONAL = [
'triggerType' => ['string'],
'branches' => ['array'],
];

/**
* Condition types for conditional questions
*/
public const CONDITION_TYPE_OPTION_SELECTED = 'option_selected';

/**
* Trigger types allowed for conditional questions
* Maps each trigger type to its supported condition types
*/
public const CONDITIONAL_TRIGGER_TYPES = [
self::ANSWER_TYPE_MULTIPLEUNIQUE => [self::CONDITION_TYPE_OPTION_SELECTED],
self::ANSWER_TYPE_DROPDOWN => [self::CONDITION_TYPE_OPTION_SELECTED],
];

/**
* !! Keep in sync with src/mixins/ShareTypes.js !!
*/
Expand Down
Loading
Loading