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
20 changes: 20 additions & 0 deletions docs/DataStructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,11 @@ 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 |
| `linearscale` | `value_equals`, `value_not_equals`, `value_range`, `value_min`, `value_max` | Linear scale - numeric value matching |
| `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`) |
| `color` | `value_equals` | Color - exact value, ignoring case |
| `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 @@ -361,6 +363,24 @@ Conditions determine when a branch is activated. The structure depends on the tr

All options in `optionIds` must be selected for the condition to match.

#### value_equals, value_not_equals (for linearscale), value_equals (for color)

```json
{ "type": "value_equals", "value": 3 }
```

```json
{ "type": "value_equals", "value": "#ff0000" }
```

#### value_range, value_min, value_max (for linearscale)

```json
{ "type": "value_range", "min": 3, "max": 5 }
```

`value_min` only uses `min`, `value_max` only uses `max`.

#### date_range (for date, datetime, time)

```json
Expand Down
7 changes: 7 additions & 0 deletions lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ class Constants {
*/
public const CONDITION_TYPE_OPTION_SELECTED = 'option_selected';
public const CONDITION_TYPE_OPTIONS_COMBINATION = 'options_combination';
public const CONDITION_TYPE_VALUE_EQUALS = 'value_equals';
public const CONDITION_TYPE_VALUE_NOT_EQUALS = 'value_not_equals';
public const CONDITION_TYPE_VALUE_RANGE = 'value_range';
public const CONDITION_TYPE_VALUE_MIN = 'value_min';
public const CONDITION_TYPE_VALUE_MAX = 'value_max';
public const CONDITION_TYPE_DATE_RANGE = 'date_range';
public const CONDITION_TYPE_FILE_UPLOADED = 'file_uploaded';

Expand All @@ -272,9 +277,11 @@ class Constants {
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],
self::ANSWER_TYPE_LINEARSCALE => [self::CONDITION_TYPE_VALUE_EQUALS, self::CONDITION_TYPE_VALUE_NOT_EQUALS, self::CONDITION_TYPE_VALUE_RANGE, self::CONDITION_TYPE_VALUE_MIN, self::CONDITION_TYPE_VALUE_MAX],
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_COLOR => [self::CONDITION_TYPE_VALUE_EQUALS],
self::ANSWER_TYPE_FILE => [self::CONDITION_TYPE_FILE_UPLOADED],
];

Expand Down
39 changes: 39 additions & 0 deletions lib/Service/SubmissionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,45 @@ private function evaluateBranchConditions(string $triggerType, array $triggerAns
}
}
return false;
case Constants::ANSWER_TYPE_LINEARSCALE:
$numValue = (float)($triggerAnswer[0] ?? 0);
foreach ($conditions as $condition) {
$type = $condition['type'] ?? Constants::CONDITION_TYPE_VALUE_EQUALS;
if ($type === Constants::CONDITION_TYPE_VALUE_EQUALS) {
if ($numValue == (float)($condition['value'] ?? 0)) {
return true;
}
} elseif ($type === Constants::CONDITION_TYPE_VALUE_NOT_EQUALS) {
if ($numValue != (float)($condition['value'] ?? 0)) {
return true;
}
} elseif ($type === Constants::CONDITION_TYPE_VALUE_RANGE) {
$min = $condition['min'] ?? -PHP_FLOAT_MAX;
$max = $condition['max'] ?? PHP_FLOAT_MAX;
if ($numValue >= $min && $numValue <= $max) {
return true;
}
} elseif ($type === Constants::CONDITION_TYPE_VALUE_MIN) {
$min = $condition['min'] ?? -PHP_FLOAT_MAX;
if ($numValue >= $min) {
return true;
}
} elseif ($type === Constants::CONDITION_TYPE_VALUE_MAX) {
$max = $condition['max'] ?? PHP_FLOAT_MAX;
if ($numValue <= $max) {
return true;
}
}
}
return false;
case Constants::ANSWER_TYPE_COLOR:
$colorValue = is_scalar($triggerAnswer[0] ?? null) ? (string)$triggerAnswer[0] : '';
foreach ($conditions as $condition) {
if (strcasecmp($colorValue, $condition['value'] ?? '') === 0) {
return true;
}
}
return false;
case Constants::ANSWER_TYPE_FILE:
$hasFile = !empty($triggerAnswer);
foreach ($conditions as $condition) {
Expand Down
134 changes: 134 additions & 0 deletions src/components/Questions/BranchConditionEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,80 @@
</div>
</template>

<!-- Value-based conditions (linearscale, color) -->
<template v-else-if="isValueBasedTrigger">
<div class="condition-row">
<template v-if="triggerType === 'linearscale'">
<NcSelect
v-model="conditionType"
:options="valueConditionTypes"
:placeholder="t('forms', 'Condition type')"
:ariaLabelCombobox="t('forms', 'Condition type')"
label="label"
:reduce="(opt) => opt.value"
class="condition-type-select" />
<template
v-if="
conditionType === 'value_equals'
|| conditionType === 'value_not_equals'
">
<NcTextField
v-model.number="conditionValue"
type="number"
:label="t('forms', 'Value')"
class="condition-value-input" />
</template>
<template v-else-if="conditionType === 'value_range'">
<NcTextField
v-model.number="conditionMin"
type="number"
:label="t('forms', 'Minimum value')"
class="condition-range-input" />
<span class="condition-range-separator">-</span>
<NcTextField
v-model.number="conditionMax"
type="number"
:label="t('forms', 'Maximum value')"
class="condition-range-input" />
</template>
<template v-else-if="conditionType === 'value_min'">
<NcTextField
v-model.number="conditionMin"
type="number"
:label="t('forms', 'Minimum value')"
class="condition-value-input" />
</template>
<template v-else-if="conditionType === 'value_max'">
<NcTextField
v-model.number="conditionMax"
type="number"
:label="t('forms', 'Maximum value')"
class="condition-value-input" />
</template>
</template>
<template v-else-if="triggerType === 'color'">
<NcColorPicker v-model="conditionValue">
<NcButton
:aria-label="
t('forms', 'Select color, current: {color}', {
color: conditionValue || t('forms', 'none'),
})
">
<template #icon>
<div
class="color-preview"
aria-hidden="true"
:style="{
backgroundColor: conditionValue || '#000000',
}" />
</template>
{{ conditionValue || t('forms', 'Select color') }}
</NcButton>
</NcColorPicker>
</template>
</div>
</template>

<!-- Date/time-based conditions -->
<template v-else-if="isDateBasedTrigger">
<div class="condition-row">
Expand Down Expand Up @@ -67,17 +141,23 @@
<script>
import { t } from '@nextcloud/l10n'
import moment from '@nextcloud/moment'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcColorPicker from '@nextcloud/vue/components/NcColorPicker'
import NcDateTimePicker from '@nextcloud/vue/components/NcDateTimePicker'
import NcSelect from '@nextcloud/vue/components/NcSelect'
import NcTextField from '@nextcloud/vue/components/NcTextField'

export default {
name: 'BranchConditionEditor',

components: {
NcButton,
NcCheckboxRadioSwitch,
NcColorPicker,
NcDateTimePicker,
NcSelect,
NcTextField,
},

props: {
Expand Down Expand Up @@ -125,6 +205,13 @@ export default {
return ['multiple_unique', 'dropdown'].includes(this.triggerType)
},

/**
* Check if trigger is value-based (linearscale, color)
*/
isValueBasedTrigger() {
return ['linearscale', 'color'].includes(this.triggerType)
},

/**
* Check if trigger is date-based (date, datetime, time)
*/
Expand Down Expand Up @@ -175,6 +262,19 @@ export default {
return this.isSingleSelect ? selected[0] || null : selected
},

/**
* Value condition type options (for linear scale)
*/
valueConditionTypes() {
return [
{ value: 'value_equals', label: t('forms', 'Equals') },
{ value: 'value_not_equals', label: t('forms', 'Not Equals') },
{ value: 'value_range', label: t('forms', 'In range') },
{ value: 'value_min', label: t('forms', 'Higher than') },
{ value: 'value_max', label: t('forms', 'Lower than') },
]
},

/**
* Current condition type from branch
*/
Expand All @@ -192,10 +292,24 @@ export default {
* Default condition type based on trigger
*/
defaultConditionType() {
if (this.triggerType === 'linearscale') return 'value_equals'
if (this.isDateBasedTrigger) return 'date_range'
return null
},

/**
* Current condition value
*/
conditionValue: {
get() {
return this.branch.conditions?.[0]?.value || ''
},

set(value) {
this.updateCondition({ value })
},
},

/**
* Storage format of date and time conditions, the same as for answers
*/
Expand Down Expand Up @@ -364,11 +478,31 @@ export default {
flex-wrap: wrap;
}

.condition-type-select {
min-width: 150px;
}

.condition-value-input {
flex: 1;
min-width: 200px;
}

.condition-range-input {
width: 100px;
}

.condition-range-separator {
color: var(--color-text-maxcontrast);
}

.condition-date-input {
flex: 1;
}

.color-preview {
width: 24px;
height: 24px;
border-radius: var(--border-radius);
border: 1px solid var(--color-border-dark);
}
</style>
31 changes: 30 additions & 1 deletion src/components/Questions/QuestionConditional.vue
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ export default defineComponent({
},
{
label: t('forms', 'Other'),
types: ['file'],
types: ['linearscale', 'color', 'file'],
},
]

Expand Down Expand Up @@ -972,6 +972,35 @@ export default defineComponent({
return false
}

if (this.triggerType === 'linearscale') {
const numValue = parseFloat(this.triggerValues[0]) || 0
return branch.conditions.some((c) => {
if (c.type === 'value_equals') {
return numValue === parseFloat(c.value)
} else if (c.type === 'value_not_equals') {
return numValue !== parseFloat(c.value)
} else if (c.type === 'value_min') {
const min = c.min ?? Number.MIN_SAFE_INTEGER
return numValue >= min
} else if (c.type === 'value_max') {
const max = c.max ?? Number.MAX_SAFE_INTEGER
return numValue <= max
} else if (c.type === 'value_range') {
const min = c.min ?? Number.MIN_SAFE_INTEGER
const max = c.max ?? Number.MAX_SAFE_INTEGER
return numValue >= min && numValue <= max
}
return false
})
}

if (this.triggerType === 'color') {
const colorValue = (this.triggerValues[0] || '').toLowerCase()
return branch.conditions.some(
(c) => colorValue === (c.value || '').toLowerCase(),
)
}

if (this.triggerType === 'file') {
const hasFile =
this.triggerValues.length > 0 && this.triggerValues[0] !== ''
Expand Down
10 changes: 10 additions & 0 deletions tests/Unit/Service/FormsServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1936,6 +1936,16 @@ public static function dataAreExtraSettingsValid() {
'questionType' => Constants::ANSWER_TYPE_CONDITIONAL,
'expected' => true
],
'valid-conditional-linearscale-trigger' => [
'extraSettings' => [
'triggerType' => Constants::ANSWER_TYPE_LINEARSCALE,
'branches' => [
['id' => 'branch-1', 'conditions' => [['type' => 'value_range', 'min' => 1, 'max' => 3]]]
]
],
'questionType' => Constants::ANSWER_TYPE_CONDITIONAL,
'expected' => true
],
'valid-conditional-date-trigger' => [
'extraSettings' => [
'triggerType' => Constants::ANSWER_TYPE_DATE,
Expand Down
Loading
Loading