Skip to content

Commit 1adac8e

Browse files
madassdevJhumanJ
andauthored
Dc3e4 new matrix field (#484)
* fix password reset bug * wip: matrix input * wip: matrix input * wip: matrix input * Fixed matric input component logic * matrix input cleanup * fix lint errors * table border and radius * cleanup, linting * fix component methos * wip matrix input * matrix condition for contains and not contain * patch matrix input condition logic * linting * refactor and cleanup * fix syntax error * Polished the matrix input * Fix linting --------- Co-authored-by: Julien Nahum <julien@nahum.net>
1 parent fedc382 commit 1adac8e

25 files changed

+919
-85
lines changed

app/Http/Requests/AnswerFormRequest.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Models\Forms\Form;
66
use App\Rules\CustomFieldValidationRule;
7+
use App\Rules\MatrixValidationRule;
78
use App\Rules\StorageFile;
89
use App\Rules\ValidHCaptcha;
910
use App\Rules\ValidPhoneInputRule;
@@ -82,9 +83,14 @@ public function rules()
8283
} elseif ($property['type'] == 'rating') {
8384
// For star rating, needs a minimum of 1 star
8485
$rules[] = 'min:1';
86+
} elseif ($property['type'] == 'matrix') {
87+
$rules[] = new MatrixValidationRule($property, true);
8588
}
8689
} else {
8790
$rules[] = 'nullable';
91+
if ($property['type'] == 'matrix') {
92+
$rules[] = new MatrixValidationRule($property, false);
93+
}
8894
}
8995

9096
// Clean id to escape "."
@@ -97,7 +103,7 @@ public function rules()
97103
}
98104

99105
// User custom validation
100-
if(!(Str::of($property['type'])->startsWith('nf-')) && isset($property['validation'])) {
106+
if (!(Str::of($property['type'])->startsWith('nf-')) && isset($property['validation'])) {
101107
$rules[] = (new CustomFieldValidationRule($property['validation'], $data));
102108
}
103109

app/Rules/FormPropertyLogicRule.php

+28
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,34 @@ class FormPropertyLogicRule implements DataAwareRule, ValidationRule
7373
],
7474
],
7575
],
76+
'matrix' => [
77+
'comparators' => [
78+
'equals' => [
79+
'expected_type' => 'object',
80+
'format' => [
81+
'type' => 'object',
82+
],
83+
],
84+
'does_not_equal' => [
85+
'expected_type' => 'object',
86+
'format' => [
87+
'type' => 'object',
88+
],
89+
],
90+
'contains' => [
91+
'expected_type' => 'object',
92+
'format' => [
93+
'type' => 'object',
94+
],
95+
],
96+
'does_not_contain' => [
97+
'expected_type' => 'object',
98+
'format' => [
99+
'type' => 'object',
100+
],
101+
],
102+
],
103+
],
76104
'url' => [
77105
'comparators' => [
78106
'equals' => [

app/Rules/MatrixValidationRule.php

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace App\Rules;
4+
5+
use Closure;
6+
use Illuminate\Contracts\Validation\ValidationRule;
7+
8+
class MatrixValidationRule implements ValidationRule
9+
{
10+
protected $field;
11+
protected $isRequired;
12+
13+
public function __construct(array $field, bool $isRequired)
14+
{
15+
$this->field = $field;
16+
$this->isRequired = $isRequired;
17+
}
18+
19+
public function validate(string $attribute, mixed $value, Closure $fail): void
20+
{
21+
if (!$this->isRequired && empty($value)) {
22+
return; // If not required and empty, validation passes
23+
}
24+
25+
if (!is_array($value)) {
26+
$fail('The Matrix field must be an array.');
27+
return;
28+
}
29+
30+
$rows = $this->field['rows'];
31+
$columns = $this->field['columns'];
32+
33+
foreach ($rows as $row) {
34+
if (!array_key_exists($row, $value)) {
35+
if ($this->isRequired) {
36+
$fail("Missing value for row '{$row}'.");
37+
}
38+
continue;
39+
}
40+
41+
$cellValue = $value[$row];
42+
43+
if ($cellValue === null) {
44+
if ($this->isRequired) {
45+
$fail("Value for row '{$row}' is required.");
46+
}
47+
continue;
48+
}
49+
50+
if (!in_array($cellValue, $columns)) {
51+
$fail("Invalid value '{$cellValue}' for row '{$row}'.");
52+
}
53+
}
54+
55+
// Check for extra rows that shouldn't be there
56+
$extraRows = array_diff(array_keys($value), $rows);
57+
foreach ($extraRows as $extraRow) {
58+
$fail("Unexpected row '{$extraRow}' in the matrix.");
59+
}
60+
}
61+
}

app/Service/Forms/FormLogicConditionChecker.php

+42
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ private function propertyConditionMet(array $propertyCondition, $value): bool
7272
return $this->multiSelectConditionMet($propertyCondition, $value);
7373
case 'files':
7474
return $this->filesConditionMet($propertyCondition, $value);
75+
case 'matrix':
76+
return $this->matrixConditionMet($propertyCondition, $value);
7577
}
7678

7779
return false;
@@ -90,6 +92,30 @@ private function checkContains($condition, $fieldValue): bool
9092
return \Str::contains($fieldValue, $condition['value']);
9193
}
9294

95+
private function checkMatrixContains($condition, $fieldValue): bool
96+
{
97+
98+
foreach($condition['value'] as $key => $value) {
99+
if(!(array_key_exists($key, $condition['value']) && array_key_exists($key, $fieldValue))) {
100+
return false;
101+
}
102+
if($condition['value'][$key] == $fieldValue[$key]) {
103+
return true;
104+
}
105+
}
106+
return false;
107+
}
108+
109+
private function checkMatrixEquals($condition, $fieldValue): bool
110+
{
111+
foreach($condition['value'] as $key => $value) {
112+
if($condition['value'][$key] !== $fieldValue[$key]) {
113+
return false;
114+
}
115+
}
116+
return true;
117+
}
118+
93119
private function checkListContains($condition, $fieldValue): bool
94120
{
95121
if (is_null($fieldValue)) {
@@ -408,4 +434,20 @@ private function filesConditionMet(array $propertyCondition, $value): bool
408434

409435
return false;
410436
}
437+
438+
private function matrixConditionMet(array $propertyCondition, $value): bool
439+
{
440+
switch ($propertyCondition['operator']) {
441+
case 'equals':
442+
return $this->checkMatrixEquals($propertyCondition, $value);
443+
case 'does_not_equal':
444+
return !$this->checkMatrixEquals($propertyCondition, $value);
445+
case 'contains':
446+
return $this->checkMatrixContains($propertyCondition, $value);
447+
case 'does_not_contain':
448+
return !$this->checkMatrixContains($propertyCondition, $value);
449+
}
450+
451+
return false;
452+
}
411453
}

app/Service/Forms/FormSubmissionFormatter.php

+17-2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ public function useSignedUrlForFiles()
8181
return $this;
8282
}
8383

84+
public function getMatrixString(array $val): string
85+
{
86+
$parts = [];
87+
foreach ($val as $key => $value) {
88+
if ($key !== null && $value !== null) {
89+
$parts[] = "$key: $value";
90+
}
91+
}
92+
return implode(' | ', $parts);
93+
}
94+
8495
/**
8596
* Return a nice "FieldName": "Field Response" array
8697
* - If createLink enabled, returns html link for emails and links
@@ -145,7 +156,9 @@ public function getCleanKeyValue()
145156
} else {
146157
$returnArray[$field['name']] = $val;
147158
}
148-
} elseif (in_array($field['type'], ['files', 'signature'])) {
159+
} elseif ($field['type'] == 'matrix' && is_array($data[$field['id']])) {
160+
$returnArray[$field['name']] = $this->getMatrixString($data[$field['id']]);
161+
} elseif ($field['type'] == 'files') {
149162
if ($this->outputStringsOnly) {
150163
$formId = $this->form->id;
151164
$returnArray[$field['name']] = implode(
@@ -219,7 +232,9 @@ public function getFieldsWithValue()
219232
} else {
220233
$field['value'] = $val;
221234
}
222-
} elseif (in_array($field['type'], ['files', 'signature'])) {
235+
} elseif ($field['type'] == 'matrix') {
236+
$field['value'] = str_replace(' | ', "\n", $this->getMatrixString($data[$field['id']]));
237+
} elseif ($field['type'] == 'files') {
223238
if ($this->outputStringsOnly) {
224239
$formId = $this->form->id;
225240
$field['value'] = implode(

client/components/forms/FlatSelectInput.vue

+37-50
Original file line numberDiff line numberDiff line change
@@ -16,60 +16,45 @@
1616
theme.default.input,
1717
theme.default.borderRadius,
1818
{
19-
'mb-2': index !== options.length,
2019
'!ring-red-500 !ring-2 !border-transparent': hasError,
2120
'!cursor-not-allowed !bg-gray-200': disabled,
2221
},
2322
]"
2423
>
25-
<template
26-
v-if="options && options.length"
27-
>
28-
<div
29-
v-for="(option) in options"
30-
:key="option[optionKey]"
31-
:role="multiple?'checkbox':'radio'"
32-
:aria-checked="isSelected(option[optionKey])"
33-
:class="[
34-
theme.FlatSelectInput.spacing.vertical,
35-
theme.FlatSelectInput.fontSize,
36-
theme.FlatSelectInput.option,
37-
]"
38-
@click="onSelect(option[optionKey])"
24+
<template
25+
v-if="options && options.length"
3926
>
40-
<template v-if="multiple">
41-
<Icon
42-
v-if="isSelected(option[optionKey])"
43-
name="material-symbols:check-box"
44-
class="text-inherit"
45-
:color="color"
46-
:class="[theme.FlatSelectInput.icon]"
47-
/>
48-
<Icon
49-
v-else
50-
name="material-symbols:check-box-outline-blank"
51-
:class="[theme.FlatSelectInput.icon,theme.FlatSelectInput.unselectedIcon]"
52-
/>
53-
</template>
54-
<template v-else>
55-
<Icon
56-
v-if="isSelected(option[optionKey])"
57-
name="material-symbols:radio-button-checked-outline"
58-
class="text-inherit"
59-
:color="color"
60-
:class="[theme.FlatSelectInput.icon]"
61-
/>
62-
<Icon
63-
v-else
64-
name="material-symbols:radio-button-unchecked"
65-
:class="[theme.FlatSelectInput.icon,theme.FlatSelectInput.unselectedIcon]"
66-
/>
67-
</template>
68-
<p class="flex-grow">
69-
{{ option[displayKey] }}
70-
</p>
71-
</div>
72-
</template>
27+
<div
28+
v-for="(option) in options"
29+
:key="option[optionKey]"
30+
:role="multiple?'checkbox':'radio'"
31+
:aria-checked="isSelected(option[optionKey])"
32+
:class="[
33+
theme.FlatSelectInput.spacing.vertical,
34+
theme.FlatSelectInput.fontSize,
35+
theme.FlatSelectInput.option,
36+
]"
37+
@click="onSelect(option[optionKey])"
38+
>
39+
<template v-if="multiple">
40+
<CheckboxIcon
41+
:is-checked="isSelected(option[optionKey])"
42+
:color="color"
43+
:theme="theme"
44+
/>
45+
</template>
46+
<template v-else>
47+
<RadioButtonIcon
48+
:is-checked="isSelected(option[optionKey])"
49+
:color="color"
50+
:theme="theme"
51+
/>
52+
</template>
53+
<p class="flex-grow">
54+
{{ option[displayKey] }}
55+
</p>
56+
</div>
57+
</template>
7358
<div
7459
v-else
7560
:class="[
@@ -96,13 +81,15 @@
9681
<script>
9782
import {inputProps, useFormInput} from "./useFormInput.js"
9883
import InputWrapper from "./components/InputWrapper.vue"
84+
import RadioButtonIcon from "./components/RadioButtonIcon.vue"
85+
import CheckboxIcon from "./components/CheckboxIcon.vue"
9986
10087
/**
10188
* Options: {name,value} objects
10289
*/
10390
export default {
10491
name: "FlatSelectInput",
105-
components: {InputWrapper},
92+
components: {InputWrapper, RadioButtonIcon, CheckboxIcon},
10693
10794
props: {
10895
...inputProps,
@@ -156,4 +143,4 @@ export default {
156143
},
157144
},
158145
}
159-
</script>
146+
</script>

0 commit comments

Comments
 (0)