Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions docs/DataStructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ Follow-up questions (subquestions) are questions of their own with additional pr
| `date` | `date_range` | Date - range including its bounds (`YYYY-MM-DD`) |
| `datetime` | `date_range` | Deprecated date and time (`YYYY-MM-DD HH:mm`) |
| `time` | `date_range` | Time - range including its bounds (`HH:mm`) |
| `file` | `file_uploaded` | File - whether a file was uploaded |

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

Expand Down Expand Up @@ -368,6 +369,14 @@ All options in `optionIds` must be selected for the condition to match.

`min` and `max` use the answer format of the question type and are optional.

#### file_uploaded (for file)

```json
{ "type": "file_uploaded", "fileUploaded": true }
```

`"fileUploaded": false` matches when no file was uploaded.

### Example

A dropdown question with a branch of follow-up questions:
Expand Down
2 changes: 2 additions & 0 deletions lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ class Constants {
public const CONDITION_TYPE_OPTION_SELECTED = 'option_selected';
public const CONDITION_TYPE_OPTIONS_COMBINATION = 'options_combination';
public const CONDITION_TYPE_DATE_RANGE = 'date_range';
public const CONDITION_TYPE_FILE_UPLOADED = 'file_uploaded';

/**
* Question types that can have branches with follow-up questions in `extraSettings.branches`,
Expand All @@ -259,6 +260,7 @@ class Constants {
self::ANSWER_TYPE_DATE => [self::CONDITION_TYPE_DATE_RANGE],
self::ANSWER_TYPE_DATETIME => [self::CONDITION_TYPE_DATE_RANGE],
self::ANSWER_TYPE_TIME => [self::CONDITION_TYPE_DATE_RANGE],
self::ANSWER_TYPE_FILE => [self::CONDITION_TYPE_FILE_UPLOADED],
];

/**
Expand Down
12 changes: 10 additions & 2 deletions lib/Service/SubmissionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -1049,8 +1049,8 @@ private function evaluateBranchConditions(string $triggerType, array $triggerAns
// Option ids arrive as numbers or strings depending on the trigger component
$selectedOptionIds = array_map('strval', array_filter($triggerAnswer, 'is_scalar'));

// An unanswered trigger activates no branch
if ($triggerAnswer === [] || ($triggerAnswer[0] ?? '') === '') {
// An unanswered trigger activates no branch, except "no file uploaded" for file triggers
if ($triggerType !== Constants::ANSWER_TYPE_FILE && ($triggerAnswer === [] || ($triggerAnswer[0] ?? '') === '')) {
return false;
}

Expand All @@ -1077,6 +1077,14 @@ private function evaluateBranchConditions(string $triggerType, array $triggerAns
}
}
return false;
case Constants::ANSWER_TYPE_FILE:
$hasFile = !empty($triggerAnswer);
foreach ($conditions as $condition) {
if (($condition['fileUploaded'] ?? true) === $hasFile) {
return true;
}
}
return false;
case Constants::ANSWER_TYPE_DATE:
case Constants::ANSWER_TYPE_DATETIME:
case Constants::ANSWER_TYPE_TIME:
Expand Down
30 changes: 30 additions & 0 deletions src/components/Conditional/BranchConditionEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,32 @@
class="condition-date-input" />
</div>
</template>

<!-- File-based conditions -->
<template v-else-if="triggerType === 'file'">
<div class="condition-row">
<NcCheckboxRadioSwitch
:modelValue="fileUploadedCondition"
@update:modelValue="onFileConditionChange">
{{ t('forms', 'File is uploaded') }}
</NcCheckboxRadioSwitch>
</div>
</template>
</div>
</template>

<script>
import { t } from '@nextcloud/l10n'
import moment from '@nextcloud/moment'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcDateTimePicker from '@nextcloud/vue/components/NcDateTimePicker'
import NcSelect from '@nextcloud/vue/components/NcSelect'

export default {
name: 'BranchConditionEditor',

components: {
NcCheckboxRadioSwitch,
NcDateTimePicker,
NcSelect,
},
Expand Down Expand Up @@ -236,6 +249,13 @@ export default {
},
},

/**
* File uploaded condition
*/
fileUploadedCondition() {
return this.branch.conditions?.[0]?.fileUploaded ?? true
},

/**
* Date picker type based on trigger type
*/
Expand Down Expand Up @@ -288,6 +308,16 @@ export default {
this.emitUpdate({ conditions })
},

/**
* Handle file condition change
*
* @param {boolean} checked Whether file is uploaded condition is checked
*/
onFileConditionChange(checked) {
const conditions = [{ fileUploaded: checked }]
this.emitUpdate({ conditions })
},

/**
* Update a condition property
*
Expand Down
1 change: 1 addition & 0 deletions src/components/Questions/QuestionFile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
</ul>
</div>
<template #insert>
<slot name="branches" />
<slot name="insert" />
</template>
</Question>
Expand Down
24 changes: 22 additions & 2 deletions src/utils/Branches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const BRANCH_MATCHERS: Record<string, BranchMatcher> = {
date: matchDateRange,
datetime: matchDateRange,
time: matchDateRange,
file: matchFileUploaded,
}

/**
Expand Down Expand Up @@ -76,6 +77,22 @@ function matchDateRange(
)
}

/**
* Files match if a condition is about the upload state of the answer, also "no file uploaded"
*
* @param conditions the conditions of the branch
* @param values the answer of the question
*/
function matchFileUploaded(
conditions: Record<string, unknown>[],
values: string[],
): boolean {
const hasFile = values.length > 0 && values[0] !== ''
return conditions.some(
(condition) => (condition.fileUploaded ?? true) === hasFile,
)
}

/**
* @param type the question type
*/
Expand Down Expand Up @@ -111,7 +128,7 @@ export function persistedSettings<T extends Record<string, unknown>>(
}

/**
* Branches the answer activates, an unanswered question activates none
* Branches the answer activates, an unanswered question activates none except "no file uploaded"
*
* @param question the question with the branches
* @param values the answer of the question
Expand All @@ -122,7 +139,10 @@ export function getActiveBranches(
): FormsConditionalBranch[] {
const matcher = BRANCH_MATCHERS[question.type]
const answer = Array.isArray(values) ? values.map(String) : []
if (!matcher || answer.length === 0 || answer[0] === '') {
if (
!matcher
|| (question.type !== 'file' && (answer.length === 0 || answer[0] === ''))
) {
return []
}
return getBranches(question).filter(
Expand Down
5 changes: 5 additions & 0 deletions tests/Unit/Service/FormsServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1935,6 +1935,11 @@ public static function dataAreExtraSettingsValid() {
'questionType' => Constants::ANSWER_TYPE_TIME,
'expected' => true
],
'valid-branches-file-uploaded' => [
'extraSettings' => ['branches' => [['id' => 'branch-1', 'conditions' => [['type' => 'file_uploaded', 'fileUploaded' => false]]]]],
'questionType' => Constants::ANSWER_TYPE_FILE,
'expected' => true
],
'valid-branches-with-own-settings' => [
'extraSettings' => ['shuffleOptions' => true, 'branches' => [['id' => 'branch-1', 'conditions' => [['type' => 'option_selected', 'optionId' => 1]]]]],
'questionType' => Constants::ANSWER_TYPE_DROPDOWN,
Expand Down
29 changes: 29 additions & 0 deletions tests/Unit/Service/SubmissionServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1755,6 +1755,35 @@ public static function dataValidateSubmission() {
// Expected Result
null,
],
'invalid-branch-no-file-uploaded' => [
// Questions - an unanswered file question opens its "no file uploaded" branch
[
[
'id' => 100,
'type' => Constants::ANSWER_TYPE_FILE,
'text' => 'File Q',
'isRequired' => false,
'extraSettings' => [
'branches' => [
[
'id' => 'branch-no-file',
'conditions' => [['type' => 'file_uploaded', 'fileUploaded' => false]],
'subQuestions' => [['id' => 101, 'type' => 'short', 'text' => 'Why no file?', 'isRequired' => true]],
],
[
'id' => 'branch-file',
'conditions' => [['type' => 'file_uploaded', 'fileUploaded' => true]],
'subQuestions' => [['id' => 102, 'type' => 'short', 'text' => 'About the file', 'isRequired' => true]],
],
],
],
],
],
// Answers - nothing uploaded
[],
// Expected Result
'Question "Why no file?" is required.',
],
'invalid-branch-unknown-option' => [
// Questions - The question is validated like any other question
[
Expand Down
Loading