Skip to content
Merged
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
32 changes: 21 additions & 11 deletions lib/Service/SubmissionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -485,9 +485,16 @@ public function validateSubmission(array $questions, array $answers, string $for
if ($question['isRequired']
&& (!$questionAnswered
|| !array_filter($answers[$questionId], static function (string|array $value): bool {
// file type
if (is_array($value)) {
return !empty($value['uploadedFileId']);
// file type
if (isset($value['uploadedFileId'])) {
return !empty($value['uploadedFileId']);
}

// Grid questions
return !empty(array_filter($value, static function ($subValue): bool {
return is_array($subValue) ? !empty(array_filter($subValue)) : $subValue !== '';
}));
}

return $value !== '';
Expand Down Expand Up @@ -557,16 +564,19 @@ public function validateSubmission(array $questions, array $answers, string $for
}
// Search corresponding option, return false if non-existent
else {
// Accept numeric strings like "46" from JSON payloads reliably (e.g. with hardening extensions enabled)
$answerId = is_int($answer) ? $answer : (is_string($answer) ? intval(trim($answer)) : null);

// Reject non-numeric / malformed values early
if ($answerId === null || (string)$answerId !== (string)intval($answerId)) {
throw new \InvalidArgumentException(sprintf('Answer "%s" for question "%s" is not a valid option.', is_scalar($answer) ? (string)$answer : gettype($answer), $question['text']));
}
$subAnswers = is_array($answer) ? $answer : [$answer];
foreach ($subAnswers as $subAnswer) {
// Accept numeric strings like "46" from JSON payloads reliably (e.g. with hardening extensions enabled)
$answerId = is_int($subAnswer) ? $subAnswer : (is_string($subAnswer) ? intval(trim($subAnswer)) : null);

// Reject non-numeric / malformed values early
if ($answerId === null || (string)$answerId !== (string)intval($answerId)) {
throw new \InvalidArgumentException(sprintf('Answer "%s" for question "%s" is not a valid option.', is_scalar($subAnswer) ? (string)$subAnswer : gettype($subAnswer), $question['text']));
}

if (!in_array($answerId, $optionIds, true)) {
throw new \InvalidArgumentException(sprintf('Answer "%s" for question "%s" is not a valid option.', $answer, $question['text']));
if (!in_array($answerId, $optionIds, true)) {
throw new \InvalidArgumentException(sprintf('Answer "%s" for question "%s" is not a valid option.', $subAnswer, $question['text']));
}
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/components/Questions/QuestionColor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
v-bind="questionProps"
:titlePlaceholder="answerType.titlePlaceholder"
:warningInvalid="answerType.warningInvalid"
:errorMessage="errorMessage"
v-on="commonListeners">
<div
class="question__content"
Expand All @@ -17,6 +18,7 @@
<NcColorPicker
:modelValue="pickedColor"
advancedFields
:aria-required="isRequired"
@update:modelValue="onUpdatePickedColor">
<NcButton :disabled="!readOnly">
{{ colorPickerPlaceholder }}
Expand Down Expand Up @@ -87,6 +89,16 @@ export default {
},

methods: {
async validate() {
if (this.isRequired && this.pickedColor === '') {
this.errorMessage = t('forms', 'You must answer this question')
return false
}

this.errorMessage = null
return true
},

onUpdatePickedColor(color) {
this.$emit('update:values', [color])
},
Expand Down
42 changes: 34 additions & 8 deletions src/components/Questions/QuestionDate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
v-bind="questionProps"
:titlePlaceholder="answerType.titlePlaceholder"
:warningInvalid="answerType.warningInvalid"
:errorMessage="errorMessage"
v-on="commonListeners">
<template v-if="answerType.pickerType === 'date'" #actions>
<NcActionCheckbox
Expand Down Expand Up @@ -95,7 +96,8 @@
:type="dateTimePickerType"
:disabledDate="disabledDates"
:disabledTime="disabledTimes"
rangeSeparator=" - "
:aria-required="isRequired"
clearable
@update:modelValue="onValueChange" />
</div>
<template #insert>
Expand Down Expand Up @@ -146,25 +148,31 @@ export default {
},

computed: {
isRangeQuestion() {
return this.extraSettings?.dateRange || this.extraSettings?.timeRange
? true
: false
},

datetimePickerPlaceholder() {
if (this.readOnly) {
return this.extraSettings?.dateRange || this.extraSettings?.timeRange
return this.isRangeQuestion
? this.answerType.submitPlaceholderRange
: this.answerType.submitPlaceholder
}
return this.extraSettings?.dateRange || this.extraSettings?.timeRange
return this.isRangeQuestion
? this.answerType.createPlaceholderRange
: this.answerType.createPlaceholder
},

dateTimePickerType() {
return this.extraSettings?.dateRange || this.extraSettings?.timeRange
return this.isRangeQuestion
? this.answerType.pickerType + '-range'
: this.answerType.pickerType
},

time() {
if (this.extraSettings?.dateRange || this.extraSettings?.timeRange) {
if (this.isRangeQuestion) {
return this.values?.[0]
? [this.parse(this.values[0]), this.parse(this.values[1])]
: null
Expand Down Expand Up @@ -224,14 +232,27 @@ export default {
},

methods: {
async validate() {
if (this.isRequired && this.time === null) {
this.errorMessage = t('forms', 'You must answer this question')
return false
}

this.errorMessage = null
return true
},

/**
* DateTimepicker show text in picker
* Format depends on component-type date/datetime
*
* @param {Date} date the selected datepicker Date
* @param {Date|Date[]} date the selected datepicker Date
* @return {string}
*/
stringify(date) {
if (this.isRangeQuestion && Array.isArray(date)) {
return `${moment(date[0]).format(this.answerType.momentFormat)} - ${moment(date[1]).format(this.answerType.momentFormat)}`
}
return moment(date).format(this.answerType.momentFormat)
},

Expand Down Expand Up @@ -335,10 +356,15 @@ export default {
/**
* Store Value
*
* @param {Date|Array<Date>} date The date or date range to store
* @param {Date|Array<Date>|null} date The date or date range to store
*/
onValueChange(date) {
if (this.extraSettings?.dateRange || this.extraSettings?.timeRange) {
if (!date) {
this.$emit('update:values', [])
return
}

if (this.isRangeQuestion) {
this.$emit('update:values', [
moment(date[0]).format(this.answerType.storageFormat),
moment(date[1]).format(this.answerType.storageFormat),
Expand Down
12 changes: 12 additions & 0 deletions src/components/Questions/QuestionDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
:warningInvalid="answerType.warningInvalid"
:contentValid="contentValid"
:shiftDragHandle="shiftDragHandle"
:errorMessage="errorMessage"
v-on="commonListeners">
<template #actions>
<NcActionCheckbox
Expand Down Expand Up @@ -40,6 +41,7 @@
:searchable="false"
label="text"
:aria-label-combobox="selectOptionPlaceholder"
@invalid.prevent="validate"
@update:modelValue="onInput" />
</div>
<template v-else>
Expand All @@ -65,7 +67,7 @@
<AnswerInput
v-for="(answer, index) in choices"
:key="answer.local ? 'option-local' : answer.id"
ref="input"

Check warning on line 70 in src/components/Questions/QuestionDropdown.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'input' is defined as ref, but never used
:answer="answer"
:formId="formId"
isDropdown
Expand Down Expand Up @@ -184,6 +186,16 @@
},

methods: {
async validate() {
if (this.isRequired && this.areNoneChecked) {
this.errorMessage = t('forms', 'You must answer this question')
return false
}

this.errorMessage = null
return true
},

onDragStart() {
this.isDragging = true
},
Expand Down
8 changes: 8 additions & 0 deletions src/components/Questions/QuestionFile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,12 @@
ref="fileInput"
class="hidden-visually"
type="file"
:required="isRequired && values.length === 0"
:disabled="!readOnly"
:multiple="maxAllowedFilesCount > 1"
:name="name || undefined"
:accept="accept.length ? accept.join(',') : null"
@invalid.prevent="validate"
@input="onFileInput" />
</label>
<NcButton
Expand Down Expand Up @@ -422,6 +424,12 @@ export default {
)
return false
}

if (this.isRequired && this.values.length === 0) {
this.errorMessage = t('forms', 'You must answer this question')
return false
}

this.errorMessage = null
return true
},
Expand Down
38 changes: 11 additions & 27 deletions src/components/Questions/QuestionGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@
<th class="first-column"></th>

<th
v-for="column of columns"
v-for="column in columns"
:key="column.local ? 'option-local' : column.id">
{{ column.text }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="row of rows"
v-for="row in rows"
:key="row.local ? 'option-local' : row.id">
<td class="first-column">{{ row.text }}</td>
<td
v-for="column of columns"
v-for="column in columns"
:key="column.local ? 'option-local' : column.id">
<template v-if="questionType === 'radio'">
<NcCheckboxRadioSwitch
Expand Down Expand Up @@ -112,7 +112,7 @@
<AnswerInput
v-for="(answer, index) in columns"
:key="answer.local ? 'option-local' : answer.id"
ref="input"

Check warning on line 115 in src/components/Questions/QuestionGrid.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'input' is defined as ref, but never used
:answer="answer"
:formId="formId"
:index="index"
Expand Down Expand Up @@ -150,7 +150,7 @@
<AnswerInput
v-for="(answer, index) in rows"
:key="answer.local ? 'option-local' : answer.id"
ref="input"

Check warning on line 153 in src/components/Questions/QuestionGrid.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'input' is defined as ref, but never used
:answer="answer"
:formId="formId"
:index="index"
Expand Down Expand Up @@ -264,6 +264,14 @@

methods: {
async validate() {
if (
this.isRequired
&& (this.values.length === 0 || this.values === null)
) {
this.errorMessage = t('forms', 'You must answer this question')
return false
}

if (!this.isUnique) {
// Validate limits
const max = this.extraSettings.optionsLimitMax ?? 0
Expand Down Expand Up @@ -315,30 +323,6 @@

this.$emit('update:values', values)
},

/**
* Is the provided answer required ?
* This is needed for checkboxes as html5
* doesn't allow to require at least ONE checked.
* So we require the one that are checked or all
* if none are checked yet.
*
* @return {boolean}
*/
checkRequired() {
// false, if question not required
if (!this.isRequired) {
return false
}

// true for Radiobuttons
if (this.isUnique) {
return true
}

// For checkboxes, only required if no other is checked
return this.areNoneChecked
},
},
}
</script>
Expand Down
32 changes: 13 additions & 19 deletions src/components/Questions/QuestionLinearScale.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
v-bind="questionProps"
:titlePlaceholder="answerType.titlePlaceholder"
:warningInvalid="answerType.warningInvalid"
:errorMessage="errorMessage"
v-on="commonListeners">
<template #actions>
<NcActionInput
Expand Down Expand Up @@ -85,7 +86,8 @@
:value="option.toString()"
:name="`${id}-answer`"
type="radio"
:required="checkRequired(option)"
:required="isRequired"
@invalid.prevent="validate"
@update:modelValue="onChange"
@keydown.enter.exact.prevent="onKeydownEnter" />
</div>
Expand Down Expand Up @@ -203,6 +205,16 @@ export default {
},

methods: {
async validate() {
if (this.isRequired && this.values.length === 0) {
this.errorMessage = t('forms', 'You must answer this question')
return false
}

this.errorMessage = null
return true
},

onChange(option) {
this.$emit('update:values', [option])
},
Expand Down Expand Up @@ -231,24 +243,6 @@ export default {
})
},

/**
* Is the provided answer required ?
* This is needed for checkboxes as html5
* doesn't allow to require at least ONE checked.
* So we require the one that are checked or all
* if none are checked yet.
*
* @return {boolean}
*/
checkRequired() {
// false, if question not required
if (!this.isRequired) {
return false
}

return true
},

/**
* Resizes the given label to fit within the specified constraints.
*
Expand Down
Loading
Loading