diff --git a/app/Http/Requests/AnswerFormRequest.php b/app/Http/Requests/AnswerFormRequest.php
index 4057c9bc5..8bfc10ee9 100644
--- a/app/Http/Requests/AnswerFormRequest.php
+++ b/app/Http/Requests/AnswerFormRequest.php
@@ -4,6 +4,7 @@
use App\Models\Forms\Form;
use App\Rules\CustomFieldValidationRule;
+use App\Rules\MatrixValidationRule;
use App\Rules\StorageFile;
use App\Rules\ValidHCaptcha;
use App\Rules\ValidPhoneInputRule;
@@ -82,9 +83,14 @@ public function rules()
} elseif ($property['type'] == 'rating') {
// For star rating, needs a minimum of 1 star
$rules[] = 'min:1';
+ } elseif ($property['type'] == 'matrix') {
+ $rules[] = new MatrixValidationRule($property, true);
}
} else {
$rules[] = 'nullable';
+ if ($property['type'] == 'matrix') {
+ $rules[] = new MatrixValidationRule($property, false);
+ }
}
// Clean id to escape "."
@@ -97,7 +103,7 @@ public function rules()
}
// User custom validation
- if(!(Str::of($property['type'])->startsWith('nf-')) && isset($property['validation'])) {
+ if (!(Str::of($property['type'])->startsWith('nf-')) && isset($property['validation'])) {
$rules[] = (new CustomFieldValidationRule($property['validation'], $data));
}
diff --git a/app/Rules/FormPropertyLogicRule.php b/app/Rules/FormPropertyLogicRule.php
index 795171a4d..cfaee33bb 100644
--- a/app/Rules/FormPropertyLogicRule.php
+++ b/app/Rules/FormPropertyLogicRule.php
@@ -73,6 +73,34 @@ class FormPropertyLogicRule implements DataAwareRule, ValidationRule
],
],
],
+ 'matrix' => [
+ 'comparators' => [
+ 'equals' => [
+ 'expected_type' => 'object',
+ 'format' => [
+ 'type' => 'object',
+ ],
+ ],
+ 'does_not_equal' => [
+ 'expected_type' => 'object',
+ 'format' => [
+ 'type' => 'object',
+ ],
+ ],
+ 'contains' => [
+ 'expected_type' => 'object',
+ 'format' => [
+ 'type' => 'object',
+ ],
+ ],
+ 'does_not_contain' => [
+ 'expected_type' => 'object',
+ 'format' => [
+ 'type' => 'object',
+ ],
+ ],
+ ],
+ ],
'url' => [
'comparators' => [
'equals' => [
diff --git a/app/Rules/MatrixValidationRule.php b/app/Rules/MatrixValidationRule.php
new file mode 100644
index 000000000..f50c1238b
--- /dev/null
+++ b/app/Rules/MatrixValidationRule.php
@@ -0,0 +1,61 @@
+field = $field;
+ $this->isRequired = $isRequired;
+ }
+
+ public function validate(string $attribute, mixed $value, Closure $fail): void
+ {
+ if (!$this->isRequired && empty($value)) {
+ return; // If not required and empty, validation passes
+ }
+
+ if (!is_array($value)) {
+ $fail('The Matrix field must be an array.');
+ return;
+ }
+
+ $rows = $this->field['rows'];
+ $columns = $this->field['columns'];
+
+ foreach ($rows as $row) {
+ if (!array_key_exists($row, $value)) {
+ if ($this->isRequired) {
+ $fail("Missing value for row '{$row}'.");
+ }
+ continue;
+ }
+
+ $cellValue = $value[$row];
+
+ if ($cellValue === null) {
+ if ($this->isRequired) {
+ $fail("Value for row '{$row}' is required.");
+ }
+ continue;
+ }
+
+ if (!in_array($cellValue, $columns)) {
+ $fail("Invalid value '{$cellValue}' for row '{$row}'.");
+ }
+ }
+
+ // Check for extra rows that shouldn't be there
+ $extraRows = array_diff(array_keys($value), $rows);
+ foreach ($extraRows as $extraRow) {
+ $fail("Unexpected row '{$extraRow}' in the matrix.");
+ }
+ }
+}
diff --git a/app/Service/Forms/FormLogicConditionChecker.php b/app/Service/Forms/FormLogicConditionChecker.php
index 11f2794d7..44e347b46 100644
--- a/app/Service/Forms/FormLogicConditionChecker.php
+++ b/app/Service/Forms/FormLogicConditionChecker.php
@@ -72,6 +72,8 @@ private function propertyConditionMet(array $propertyCondition, $value): bool
return $this->multiSelectConditionMet($propertyCondition, $value);
case 'files':
return $this->filesConditionMet($propertyCondition, $value);
+ case 'matrix':
+ return $this->matrixConditionMet($propertyCondition, $value);
}
return false;
@@ -90,6 +92,30 @@ private function checkContains($condition, $fieldValue): bool
return \Str::contains($fieldValue, $condition['value']);
}
+ private function checkMatrixContains($condition, $fieldValue): bool
+ {
+
+ foreach($condition['value'] as $key => $value) {
+ if(!(array_key_exists($key, $condition['value']) && array_key_exists($key, $fieldValue))) {
+ return false;
+ }
+ if($condition['value'][$key] == $fieldValue[$key]) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private function checkMatrixEquals($condition, $fieldValue): bool
+ {
+ foreach($condition['value'] as $key => $value) {
+ if($condition['value'][$key] !== $fieldValue[$key]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private function checkListContains($condition, $fieldValue): bool
{
if (is_null($fieldValue)) {
@@ -408,4 +434,20 @@ private function filesConditionMet(array $propertyCondition, $value): bool
return false;
}
+
+ private function matrixConditionMet(array $propertyCondition, $value): bool
+ {
+ switch ($propertyCondition['operator']) {
+ case 'equals':
+ return $this->checkMatrixEquals($propertyCondition, $value);
+ case 'does_not_equal':
+ return !$this->checkMatrixEquals($propertyCondition, $value);
+ case 'contains':
+ return $this->checkMatrixContains($propertyCondition, $value);
+ case 'does_not_contain':
+ return !$this->checkMatrixContains($propertyCondition, $value);
+ }
+
+ return false;
+ }
}
diff --git a/app/Service/Forms/FormSubmissionFormatter.php b/app/Service/Forms/FormSubmissionFormatter.php
index b21317704..5b123c854 100644
--- a/app/Service/Forms/FormSubmissionFormatter.php
+++ b/app/Service/Forms/FormSubmissionFormatter.php
@@ -81,6 +81,17 @@ public function useSignedUrlForFiles()
return $this;
}
+ public function getMatrixString(array $val): string
+ {
+ $parts = [];
+ foreach ($val as $key => $value) {
+ if ($key !== null && $value !== null) {
+ $parts[] = "$key: $value";
+ }
+ }
+ return implode(' | ', $parts);
+ }
+
/**
* Return a nice "FieldName": "Field Response" array
* - If createLink enabled, returns html link for emails and links
@@ -145,7 +156,9 @@ public function getCleanKeyValue()
} else {
$returnArray[$field['name']] = $val;
}
- } elseif (in_array($field['type'], ['files', 'signature'])) {
+ } elseif ($field['type'] == 'matrix' && is_array($data[$field['id']])) {
+ $returnArray[$field['name']] = $this->getMatrixString($data[$field['id']]);
+ } elseif ($field['type'] == 'files') {
if ($this->outputStringsOnly) {
$formId = $this->form->id;
$returnArray[$field['name']] = implode(
@@ -219,7 +232,9 @@ public function getFieldsWithValue()
} else {
$field['value'] = $val;
}
- } elseif (in_array($field['type'], ['files', 'signature'])) {
+ } elseif ($field['type'] == 'matrix') {
+ $field['value'] = str_replace(' | ', "\n", $this->getMatrixString($data[$field['id']]));
+ } elseif ($field['type'] == 'files') {
if ($this->outputStringsOnly) {
$formId = $this->form->id;
$field['value'] = implode(
diff --git a/client/components/forms/FlatSelectInput.vue b/client/components/forms/FlatSelectInput.vue
index a417bee20..7f6b4c414 100644
--- a/client/components/forms/FlatSelectInput.vue
+++ b/client/components/forms/FlatSelectInput.vue
@@ -16,60 +16,45 @@
theme.default.input,
theme.default.borderRadius,
{
- 'mb-2': index !== options.length,
'!ring-red-500 !ring-2 !border-transparent': hasError,
'!cursor-not-allowed !bg-gray-200': disabled,
},
]"
>
-
-
-
-
-
-
-
-
-
-
-
- {{ option[displayKey] }}
-
-
-
+
+
+
+
+
+
+
+
+ {{ option[displayKey] }}
+
+
+
+
\ No newline at end of file
diff --git a/client/components/forms/MatrixInput.vue b/client/components/forms/MatrixInput.vue
new file mode 100644
index 000000000..25cdad39f
--- /dev/null
+++ b/client/components/forms/MatrixInput.vue
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ column }}
+
+
+
+
+
+
+
+
+
+ {{ row }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/components/forms/components/CheckboxIcon.vue b/client/components/forms/components/CheckboxIcon.vue
new file mode 100644
index 000000000..2c80db4e0
--- /dev/null
+++ b/client/components/forms/components/CheckboxIcon.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/client/components/forms/components/RadioButtonIcon.vue b/client/components/forms/components/RadioButtonIcon.vue
new file mode 100644
index 000000000..157971945
--- /dev/null
+++ b/client/components/forms/components/RadioButtonIcon.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/client/components/open/forms/OpenForm.vue b/client/components/open/forms/OpenForm.vue
index e20cbe458..f2ef896f7 100644
--- a/client/components/open/forms/OpenForm.vue
+++ b/client/components/open/forms/OpenForm.vue
@@ -448,6 +448,8 @@ export default {
formData[field.id] = urlPrefill.getAll(field.id + '[]')
} else if (field.type === 'date' && field.prefill_today === true) { // For Prefill with 'today'
formData[field.id] = new Date().toISOString()
+ } else if (field.type === 'matrix') {
+ formData[field.id] = {...field.prefill}
} else { // Default prefill if any
formData[field.id] = field.prefill
}
diff --git a/client/components/open/forms/OpenFormField.vue b/client/components/open/forms/OpenFormField.vue
index 67ea37464..5022df793 100644
--- a/client/components/open/forms/OpenFormField.vue
+++ b/client/components/open/forms/OpenFormField.vue
@@ -6,7 +6,7 @@
:class="[
getFieldWidthClasses(field),
{
- 'group/nffield hover:bg-gray-100/50 relative hover:z-10 transition-colors hover:border-gray-200 dark:hover:bg-gray-900 border-dashed border border-transparent box-border dark:hover:border-blue-900 rounded-md':adminPreview,
+ 'group/nffield hover:bg-gray-100/50 relative hover:z-10 transition-colors hover:border-gray-200 dark:hover:!bg-gray-900 border-dashed border border-transparent box-border dark:hover:border-blue-900 rounded-md':adminPreview,
'bg-blue-50 hover:!bg-blue-50 dark:bg-gray-800 rounded-md': beingEdited,
}]"
>
@@ -95,8 +95,12 @@
>
-
-
+
+
{
diff --git a/client/components/open/forms/components/form-components/AddFormBlock.vue b/client/components/open/forms/components/form-components/AddFormBlock.vue
index 562269e3b..18afe5407 100644
--- a/client/components/open/forms/components/form-components/AddFormBlock.vue
+++ b/client/components/open/forms/components/form-components/AddFormBlock.vue
@@ -204,6 +204,11 @@ export default {
title: "Signature Input",
icon: ' ',
},
+ {
+ name: "matrix",
+ title: "Matrix Input",
+ icon: ' ',
+ },
],
layoutBlocks: [
{
diff --git a/client/components/open/forms/components/form-logic-components/ColumnCondition.vue b/client/components/open/forms/components/form-logic-components/ColumnCondition.vue
index bc541d75b..a64f9d3fa 100644
--- a/client/components/open/forms/components/form-logic-components/ColumnCondition.vue
+++ b/client/components/open/forms/components/form-logic-components/ColumnCondition.vue
@@ -58,6 +58,7 @@ export default {
url: "TextInput",
email: "TextInput",
phone_number: "TextInput",
+ matrix: "MatrixInput",
},
}
},
@@ -93,6 +94,10 @@ export default {
} else if (this.property.type === "checkbox") {
componentData.label = this.property.name
}
+ else if (this.property.type === "matrix"){
+ componentData.rows = this.property.rows
+ componentData.columns = this.property.columns
+ }
return componentData
},
@@ -184,8 +189,9 @@ export default {
) {
this.content.value = {}
} else if (
- typeof this.content.value === "boolean" ||
- typeof this.content.value === "object"
+ this.property.type !== 'matrix' &&
+ (typeof this.content.value === 'boolean' ||
+ typeof this.content.value === 'object')
) {
this.content.value = null
}
diff --git a/client/components/open/forms/fields/components/FieldOptions.vue b/client/components/open/forms/fields/components/FieldOptions.vue
index 248a463e7..dbfb1a73d 100644
--- a/client/components/open/forms/fields/components/FieldOptions.vue
+++ b/client/components/open/forms/fields/components/FieldOptions.vue
@@ -187,6 +187,11 @@
/>
+
+
+
+
+
-
{
- return option.name
- }).join('\n')
- }
},
watch: {
@@ -868,6 +876,13 @@ export default {
date: {
date_format: this.dateFormatOptions[0].value,
time_format: this.timeFormatOptions[0].value
+ },
+ matrix: {
+ rows:['Row 1'],
+ columns: [1 ,2 ,3],
+ selection_data:{
+ 'Row 1': null
+ }
}
}
if (this.field.type in defaultFieldValues) {
@@ -877,6 +892,9 @@ export default {
}
})
}
+ },
+ updateMatrixField(newField) {
+ this.field = newField
}
}
}
diff --git a/client/components/open/forms/fields/components/MatrixFieldOptions.vue b/client/components/open/forms/fields/components/MatrixFieldOptions.vue
new file mode 100644
index 000000000..cb654e3cc
--- /dev/null
+++ b/client/components/open/forms/fields/components/MatrixFieldOptions.vue
@@ -0,0 +1,132 @@
+
+
+
+ Matrix
+
+
+ Advanced options for matrix.
+
+
+
+
+
+
+
+
+
+
+
+ Add row
+
+
+
+
+
+
+
+
+
+
+ Add column
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/components/open/forms/fields/components/MatrixPrefilledValues.vue b/client/components/open/forms/fields/components/MatrixPrefilledValues.vue
new file mode 100644
index 000000000..11bf47f11
--- /dev/null
+++ b/client/components/open/forms/fields/components/MatrixPrefilledValues.vue
@@ -0,0 +1,52 @@
+
+ Prefilled values
+
+
+
\ No newline at end of file
diff --git a/client/components/open/tables/OpenTable.vue b/client/components/open/tables/OpenTable.vue
index a541cfa1f..9807d7f2a 100644
--- a/client/components/open/tables/OpenTable.vue
+++ b/client/components/open/tables/OpenTable.vue
@@ -140,6 +140,7 @@
import OpenText from "./components/OpenText.vue"
import OpenUrl from "./components/OpenUrl.vue"
import OpenSelect from "./components/OpenSelect.vue"
+import OpenMatrix from "./components/OpenMatrix.vue"
import OpenDate from "./components/OpenDate.vue"
import OpenFile from "./components/OpenFile.vue"
import OpenCheckbox from "./components/OpenCheckbox.vue"
@@ -197,6 +198,7 @@ export default {
scale: shallowRef(OpenText),
slider: shallowRef(OpenText),
select: shallowRef(OpenSelect),
+ matrix: shallowRef(OpenMatrix),
multi_select: shallowRef(OpenSelect),
date: shallowRef(OpenDate),
files: shallowRef(OpenFile),
diff --git a/client/components/open/tables/components/OpenMatrix.vue b/client/components/open/tables/components/OpenMatrix.vue
new file mode 100644
index 000000000..f8b68755f
--- /dev/null
+++ b/client/components/open/tables/components/OpenMatrix.vue
@@ -0,0 +1,50 @@
+
+
+
+
+ {{ data.label }}
+ {{ data.value }}
+
+
+
+
+
+
+
+
diff --git a/client/data/open_filters.json b/client/data/open_filters.json
index 3d72525e3..09826647e 100644
--- a/client/data/open_filters.json
+++ b/client/data/open_filters.json
@@ -231,6 +231,34 @@
}
}
},
+ "matrix": {
+ "comparators": {
+ "equals": {
+ "expected_type": "object",
+ "format": {
+ "type": "object"
+ }
+ },
+ "does_not_equal": {
+ "expected_type": "object",
+ "format": {
+ "type":"object"
+ }
+ },
+ "contains": {
+ "expected_type": "object",
+ "format": {
+ "type":"object"
+ }
+ },
+ "does_not_contain": {
+ "expected_type": "object",
+ "format": {
+ "type":"object"
+ }
+ }
+ }
+ },
"number": {
"comparators": {
"equals": {
diff --git a/client/lib/forms/FormLogicConditionChecker.js b/client/lib/forms/FormLogicConditionChecker.js
index f80aef114..d75b891fa 100644
--- a/client/lib/forms/FormLogicConditionChecker.js
+++ b/client/lib/forms/FormLogicConditionChecker.js
@@ -1,3 +1,5 @@
+import { default as _isEqual } from "lodash/isEqual"
+
export function conditionsMet(conditions, formData) {
if (conditions === undefined || conditions === null) {
return false
@@ -59,6 +61,8 @@ function propertyConditionMet(propertyCondition, value) {
return multiSelectConditionMet(propertyCondition, value)
case "files":
return filesConditionMet(propertyCondition, value)
+ case "matrix":
+ return matrixConditionMet(propertyCondition, value)
}
return false
}
@@ -67,6 +71,24 @@ function checkEquals(condition, fieldValue) {
return condition.value === fieldValue
}
+function checkObjectEquals(condition, fieldValue) {
+ return _isEqual(condition.value, fieldValue)
+}
+
+function checkMatrixContains(condition, fieldValue)
+{
+ if (typeof fieldValue === "undefined" || typeof fieldValue !== "object") {
+ return false
+ }
+ const conditionValue = condition.value
+ for (const key in conditionValue) {
+ if(conditionValue[key] == fieldValue[key]){
+ return true
+ }
+ }
+ return false
+}
+
function checkContains(condition, fieldValue) {
return fieldValue ? fieldValue.includes(condition.value) : false
}
@@ -148,7 +170,7 @@ function checkPastWeek(condition, fieldValue) {
return (
fieldDate <= today &&
fieldDate >=
- new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7)
+ new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7)
)
}
@@ -159,7 +181,7 @@ function checkPastMonth(condition, fieldValue) {
return (
fieldDate <= today &&
fieldDate >=
- new Date(today.getFullYear(), today.getMonth() - 1, today.getDate())
+ new Date(today.getFullYear(), today.getMonth() - 1, today.getDate())
)
}
@@ -170,7 +192,7 @@ function checkPastYear(condition, fieldValue) {
return (
fieldDate <= today &&
fieldDate >=
- new Date(today.getFullYear() - 1, today.getMonth(), today.getDate())
+ new Date(today.getFullYear() - 1, today.getMonth(), today.getDate())
)
}
@@ -181,7 +203,7 @@ function checkNextWeek(condition, fieldValue) {
return (
fieldDate >= today &&
fieldDate <=
- new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7)
+ new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7)
)
}
@@ -192,7 +214,7 @@ function checkNextMonth(condition, fieldValue) {
return (
fieldDate >= today &&
fieldDate <=
- new Date(today.getFullYear(), today.getMonth() + 1, today.getDate())
+ new Date(today.getFullYear(), today.getMonth() + 1, today.getDate())
)
}
@@ -203,7 +225,7 @@ function checkNextYear(condition, fieldValue) {
return (
fieldDate >= today &&
fieldDate <=
- new Date(today.getFullYear() + 1, today.getMonth(), today.getDate())
+ new Date(today.getFullYear() + 1, today.getMonth(), today.getDate())
)
}
@@ -371,3 +393,17 @@ function filesConditionMet(propertyCondition, value) {
}
return false
}
+
+function matrixConditionMet(propertyCondition, value) {
+ switch (propertyCondition.operator) {
+ case "equals":
+ return checkObjectEquals(propertyCondition, value)
+ case "does_not_equal":
+ return !checkObjectEquals(propertyCondition, value)
+ case "contains":
+ return checkMatrixContains(propertyCondition, value)
+ case "does_not_contain":
+ return !checkMatrixContains(propertyCondition, value)
+ }
+ return false
+}
diff --git a/client/lib/forms/FormPropertyLogicRule.js b/client/lib/forms/FormPropertyLogicRule.js
index a1ae5c1c8..49df65d1d 100644
--- a/client/lib/forms/FormPropertyLogicRule.js
+++ b/client/lib/forms/FormPropertyLogicRule.js
@@ -107,7 +107,7 @@ class FormPropertyLogicRule {
(type === "string" && typeof value !== "string") ||
(type === "boolean" && typeof value !== "boolean") ||
(type === "number" && typeof value !== "number") ||
- (type === "object" && !Array.isArray(value))
+ (type === "object" && !(Array.isArray(value) || typeof value === 'object'))
) {
return false
}
diff --git a/client/stores/working_form.js b/client/stores/working_form.js
index efabbcb21..61a8a445c 100644
--- a/client/stores/working_form.js
+++ b/client/stores/working_form.js
@@ -17,6 +17,7 @@ const defaultBlockNames = {
multi_select: "Multi Select",
files: "Files",
signature: "Signature",
+ matrix: "Matrix",
"nf-text": "Text Block",
"nf-page-break": "Page Break",
"nf-divider": "Divider",
diff --git a/database/factories/FormFactory.php b/database/factories/FormFactory.php
index 05586f549..a8d91d9d9 100644
--- a/database/factories/FormFactory.php
+++ b/database/factories/FormFactory.php
@@ -21,15 +21,6 @@ public function forWorkspace(Workspace $workspace)
});
}
- public function forDatabase(string $databaseId)
- {
- return $this->state(function (array $attributes) use ($databaseId) {
- return [
- 'database_id' => $databaseId,
- ];
- });
- }
-
public function withProperties(array $properties)
{
return $this->state(function (array $attributes) use ($properties) {
diff --git a/resources/views/mail/form/submission-notification.blade.php b/resources/views/mail/form/submission-notification.blade.php
index f421359bf..80b28c72c 100644
--- a/resources/views/mail/form/submission-notification.blade.php
+++ b/resources/views/mail/form/submission-notification.blade.php
@@ -16,7 +16,7 @@
{{$link['label']}}
@endforeach
@else
-{!! is_array($field['value'])?implode(',',$field['value']):$field['value']!!}
+{!! is_array($field['value'])?implode(',',$field['value']):nl2br(e($field['value']))!!}
@endif
@endif
@endforeach
diff --git a/tests/Feature/Forms/MatrixInputTest.php b/tests/Feature/Forms/MatrixInputTest.php
new file mode 100644
index 000000000..6215bb8bb
--- /dev/null
+++ b/tests/Feature/Forms/MatrixInputTest.php
@@ -0,0 +1,185 @@
+actingAsUser();
+ $workspace = $this->createUserWorkspace($user);
+ $form = $this->createForm($user, $workspace);
+
+ $matrixProperty = [
+ 'id' => 'matrix_field',
+ 'name' => 'Matrix Question',
+ 'type' => 'matrix',
+ 'rows' => ['Row 1', 'Row 2', 'Row 3'],
+ 'columns' => ['Column A', 'Column B', 'Column C'],
+ 'required' => true
+ ];
+
+ $form->properties = array_merge($form->properties, [$matrixProperty]);
+ $form->update();
+
+ $submissionData = [
+ 'matrix_field' => [
+ 'Row 1' => 'Column A',
+ 'Row 2' => 'Column B',
+ 'Row 3' => 'Column C'
+ ]
+ ];
+
+ $formData = FormSubmissionDataFactory::generateSubmissionData($form, $submissionData);
+
+ $this->postJson(route('forms.answer', $form->slug), $formData)
+ ->assertSuccessful()
+ ->assertJson([
+ 'type' => 'success',
+ 'message' => 'Form submission saved.',
+ ]);
+});
+
+it('cannot submit form with invalid matrix input', function () {
+ $user = $this->actingAsUser();
+ $workspace = $this->createUserWorkspace($user);
+ $form = $this->createForm($user, $workspace);
+
+ $matrixProperty = [
+ 'id' => 'matrix_field',
+ 'name' => 'Matrix Question',
+ 'type' => 'matrix',
+ 'rows' => ['Row 1', 'Row 2', 'Row 3'],
+ 'columns' => ['Column A', 'Column B', 'Column C'],
+ 'required' => true
+ ];
+
+ $form->properties = array_merge($form->properties, [$matrixProperty]);
+ $form->update();
+
+ $submissionData = [
+ 'matrix_field' => [
+ 'Row 1' => 'Column A',
+ 'Row 2' => 'Invalid Column',
+ 'Row 3' => 'Column C'
+ ]
+ ];
+
+ $formData = FormSubmissionDataFactory::generateSubmissionData($form, $submissionData);
+
+ $this->postJson(route('forms.answer', $form->slug), $formData)
+ ->assertStatus(422)
+ ->assertJson([
+ 'message' => "Invalid value 'Invalid Column' for row 'Row 2'.",
+ 'errors' => [
+ 'matrix_field' => [
+ "Invalid value 'Invalid Column' for row 'Row 2'."
+ ]
+ ]
+ ]);
+});
+
+it('can submit form with optional matrix input left empty', function () {
+ $user = $this->actingAsUser();
+ $workspace = $this->createUserWorkspace($user);
+ $form = $this->createForm($user, $workspace);
+
+ $matrixProperty = [
+ 'id' => 'matrix_field',
+ 'name' => 'Matrix Question',
+ 'type' => 'matrix',
+ 'rows' => ['Row 1', 'Row 2', 'Row 3'],
+ 'columns' => ['Column A', 'Column B', 'Column C'],
+ 'required' => false
+ ];
+
+ $form->properties = array_merge($form->properties, [$matrixProperty]);
+ $form->update();
+
+ $submissionData = [
+ 'matrix_field' => []
+ ];
+
+ $formData = FormSubmissionDataFactory::generateSubmissionData($form, $submissionData);
+
+ $this->postJson(route('forms.answer', $form->slug), $formData)
+ ->assertSuccessful()
+ ->assertJson([
+ 'type' => 'success',
+ 'message' => 'Form submission saved.',
+ ]);
+});
+
+it('cannot submit form with required matrix input left empty', function () {
+ $user = $this->actingAsUser();
+ $workspace = $this->createUserWorkspace($user);
+ $form = $this->createForm($user, $workspace);
+
+ $matrixProperty = [
+ 'id' => 'matrix_field',
+ 'name' => 'Matrix Question',
+ 'type' => 'matrix',
+ 'rows' => ['Row 1', 'Row 2', 'Row 3'],
+ 'columns' => ['Column A', 'Column B', 'Column C'],
+ 'required' => true
+ ];
+
+ $form->properties = array_merge($form->properties, [$matrixProperty]);
+ $form->update();
+
+ $submissionData = [
+ 'matrix_field' => []
+ ];
+
+ $formData = FormSubmissionDataFactory::generateSubmissionData($form, $submissionData);
+
+ $this->postJson(route('forms.answer', $form->slug), $formData)
+ ->assertStatus(422)
+ ->assertJson([
+ 'message' => 'The Matrix Question field is required.',
+ 'errors' => [
+ 'matrix_field' => [
+ 'The Matrix Question field is required.'
+ ]
+ ]
+ ]);
+});
+
+it('can validate matrix input with precognition', function () {
+ $user = $this->actingAsUser();
+ $workspace = $this->createUserWorkspace($user);
+ $form = $this->createForm($user, $workspace);
+
+ $matrixProperty = [
+ 'id' => 'matrix_field',
+ 'name' => 'Matrix Question',
+ 'type' => 'matrix',
+ 'rows' => ['Row 1', 'Row 2', 'Row 3'],
+ 'columns' => ['Column A', 'Column B', 'Column C'],
+ 'required' => true
+ ];
+
+ $form->properties = array_merge($form->properties, [$matrixProperty]);
+ $form->update();
+
+ $submissionData = [
+ 'matrix_field' => [
+ 'Row 1' => 'Column A',
+ 'Row 2' => 'Invalid Column',
+ 'Row 3' => 'Column C'
+ ]
+ ];
+
+ $formData = FormSubmissionDataFactory::generateSubmissionData($form, $submissionData);
+
+ $response = $this->withPrecognition()->withHeaders([
+ 'Precognition-Validate-Only' => 'matrix_field'
+ ])
+ ->postJson(route('forms.answer', $form->slug), $formData);
+
+ $response->assertStatus(422)
+ ->assertJson([
+ 'errors' => [
+ 'matrix_field' => [
+ 'Invalid value \'Invalid Column\' for row \'Row 2\'.'
+ ]
+ ]
+ ]);
+});