$emit('updated', row, value)"
diff --git a/resources/js/components/fieldtypes/grid/StackedRow.vue b/resources/js/components/fieldtypes/grid/StackedRow.vue
index 032ef4fc85..3d15468963 100644
--- a/resources/js/components/fieldtypes/grid/StackedRow.vue
+++ b/resources/js/components/fieldtypes/grid/StackedRow.vue
@@ -22,7 +22,7 @@
:parent-name="name"
:set-index="index"
:errors="errors(field.handle)"
- :error-key-prefix="errorKey(field.handle)"
+ :field-path-prefix="fieldPath(field.handle)"
class="p-2"
:read-only="grid.isReadOnly"
@updated="updated(field.handle, $event)"
diff --git a/resources/js/components/fieldtypes/grid/Table.vue b/resources/js/components/fieldtypes/grid/Table.vue
index 19d62def74..2c10f5c91b 100644
--- a/resources/js/components/fieldtypes/grid/Table.vue
+++ b/resources/js/components/fieldtypes/grid/Table.vue
@@ -30,7 +30,7 @@
:values="row"
:meta="meta[row._id]"
:name="name"
- :error-key-prefix="errorKeyPrefix"
+ :field-path-prefix="fieldPathPrefix"
:can-delete="canDeleteRows"
:can-add-rows="canAddRows"
@updated="(row, value) => $emit('updated', row, value)"
diff --git a/resources/js/components/fieldtypes/grid/View.vue b/resources/js/components/fieldtypes/grid/View.vue
index 7b421c162b..eca8f562e2 100644
--- a/resources/js/components/fieldtypes/grid/View.vue
+++ b/resources/js/components/fieldtypes/grid/View.vue
@@ -15,8 +15,8 @@ export default {
return `${this.name}-drag-handle`;
},
- errorKeyPrefix() {
- return this.grid.errorKeyPrefix || this.grid.handle;
+ fieldPathPrefix() {
+ return this.grid.fieldPathPrefix || this.grid.handle;
}
},
diff --git a/resources/js/components/fieldtypes/replicator/Field.vue b/resources/js/components/fieldtypes/replicator/Field.vue
index daba8b3908..7cb9f5073b 100644
--- a/resources/js/components/fieldtypes/replicator/Field.vue
+++ b/resources/js/components/fieldtypes/replicator/Field.vue
@@ -20,7 +20,7 @@
:value="value"
:handle="field.handle"
:name-prefix="namePrefix"
- :error-key-prefix="errorKey"
+ :field-path-prefix="fieldPath"
:has-error="hasError || hasNestedError"
:read-only="isReadOnly"
@input="$emit('updated', $event)"
@@ -65,7 +65,7 @@ export default {
type: Number,
required: true
},
- errorKey: {
+ fieldPath: {
type: String
},
readOnly: Boolean,
@@ -98,7 +98,7 @@ export default {
},
errors() {
- return this.storeState.errors[this.errorKey] || [];
+ return this.storeState.errors[this.fieldPath] || [];
},
hasError() {
@@ -106,7 +106,7 @@ export default {
},
hasNestedError() {
- const prefix = `${this.errorKey}.`;
+ const prefix = `${this.fieldPath}.`;
return Object.keys(this.storeState.errors).some(handle => handle.startsWith(prefix));
},
diff --git a/resources/js/components/fieldtypes/replicator/Replicator.vue b/resources/js/components/fieldtypes/replicator/Replicator.vue
index 9c917d089e..c1fd58de12 100644
--- a/resources/js/components/fieldtypes/replicator/Replicator.vue
+++ b/resources/js/components/fieldtypes/replicator/Replicator.vue
@@ -30,7 +30,7 @@
:sortable-handle-class="sortableHandleClass"
:is-read-only="isReadOnly"
:collapsed="collapsed.includes(set._id)"
- :error-key-prefix="errorKeyPrefix || handle"
+ :field-path-prefix="fieldPathPrefix || handle"
:has-error="setHasError(index)"
:previews="previews[set._id]"
@collapsed="collapseSet(set._id)"
@@ -90,7 +90,7 @@ export default {
},
computed: {
-
+
previews() {
return this.meta.previews;
},
@@ -204,7 +204,7 @@ export default {
},
setHasError(index) {
- const prefix = `${this.errorKeyPrefix || this.handle}.${index}.`;
+ const prefix = `${this.fieldPathPrefix || this.handle}.${index}.`;
return Object.keys(this.storeState.errors ?? []).some(handle => handle.startsWith(prefix));
},
diff --git a/resources/js/components/fieldtypes/replicator/Set.vue b/resources/js/components/fieldtypes/replicator/Set.vue
index 000b807182..6f6871123d 100644
--- a/resources/js/components/fieldtypes/replicator/Set.vue
+++ b/resources/js/components/fieldtypes/replicator/Set.vue
@@ -35,14 +35,14 @@
handle.startsWith(prefix));
},
diff --git a/resources/js/tests/FieldConditionsOmitter.test.js b/resources/js/tests/FieldConditionsOmitter.test.js
new file mode 100644
index 0000000000..9cf05dd695
--- /dev/null
+++ b/resources/js/tests/FieldConditionsOmitter.test.js
@@ -0,0 +1,207 @@
+import Omitter from '../components/field-conditions/Omitter.js';
+
+test('it omits values at top level', () => {
+ let values = {
+ first_name: 'Han',
+ last_name: 'Solo',
+ ship: 'Falcon',
+ bff: 'Chewy',
+ };
+
+ let omitted = new Omitter(values).omit([
+ 'last_name',
+ 'bff',
+ ]);
+
+ let expected = {
+ first_name: 'Han',
+ ship: 'Falcon',
+ };
+
+ expect(new Omitter(values).omit(['last_name', 'bff'])).toEqual(expected);
+});
+
+test('it omits nested values', () => {
+ let values = {
+ first_name: 'Han',
+ last_name: 'Solo',
+ ship: {
+ name: 'Falcon',
+ completed_kessel_run: 'less than 12 parsecs',
+ junk: true,
+ },
+ bffs: [
+ {
+ name: 'Chewy',
+ type: 'Wookie',
+ },
+ {
+ name: 'Leia',
+ type: 'Woman',
+ crush: [
+ {
+ name: 'Lando',
+ type: 'Man',
+ }
+ ],
+ },
+ ],
+ };
+
+ let omitted = new Omitter(values).omit([
+ 'last_name',
+ 'ship.completed_kessel_run',
+ 'bffs.0.type',
+ 'bffs.1.crush.0.name',
+ ]);
+
+ let expected = {
+ first_name: 'Han',
+ ship: {
+ name: 'Falcon',
+ junk: true,
+ },
+ bffs: [
+ {
+ name: 'Chewy',
+ },
+ {
+ name: 'Leia',
+ type: 'Woman',
+ crush: [
+ {
+ type: 'Man',
+ }
+ ],
+ },
+ ],
+ };
+
+ expect(omitted).toEqual(expected);
+});
+
+test('it omits nested json field values', () => {
+ let values = {
+ first_name: 'Han',
+ last_name: 'Solo',
+ ship: {
+ name: 'Falcon',
+ completed_kessel_run: 'less than 12 parsecs',
+ junk: true,
+ },
+ bffs: JSON.stringify([
+ {
+ name: 'Chewy',
+ type: 'Wookie',
+ },
+ {
+ name: 'Leia',
+ type: 'Woman',
+ crush: JSON.stringify([
+ {
+ name: 'Lando',
+ type: 'Man',
+ },
+ ]),
+ },
+ ]),
+ };
+
+ let jsonFields = [
+ 'bffs.1.crush', // Intentionally passing deeper JSON value first to ensure the Omitter properly sorts these before decoding
+ 'bffs',
+ ];
+
+ let omitted = new Omitter(values, jsonFields).omit([
+ 'last_name',
+ 'ship.completed_kessel_run',
+ 'bffs.0.type',
+ 'bffs.1.crush.0.name',
+ ]);
+
+ let expected = {
+ first_name: 'Han',
+ ship: {
+ name: 'Falcon',
+ junk: true,
+ },
+ bffs: JSON.stringify([
+ {
+ name: 'Chewy',
+ },
+ {
+ name: 'Leia',
+ type: 'Woman',
+ crush: JSON.stringify([
+ {
+ type: 'Man',
+ },
+ ]),
+ },
+ ]),
+ };
+
+ expect(omitted).toEqual(expected);
+});
+
+test('it omits null hidden values', () => {
+ let values = {
+ first_name: 'Han',
+ last_name: 'Solo',
+ ship: 'Falcon',
+ bff: null, // this is null, but should still get removed
+ };
+
+ let omitted = new Omitter(values).omit([
+ 'last_name',
+ 'bff',
+ ]);
+
+ let expected = {
+ first_name: 'Han',
+ ship: 'Falcon',
+ };
+
+ expect(new Omitter(values).omit(['last_name', 'bff'])).toEqual(expected);
+});
+
+test('it gracefully handles errors', () => {
+ let values = {
+ first_name: 'Han',
+ last_name: 'Solo',
+ bffs: JSON.stringify([
+ {
+ name: 'Chewy',
+ type: 'Wookie',
+ },
+ ]),
+ };
+
+ let jsonFields = [
+ 'bffs',
+ 'bffs', // duplicate field
+ 'middle_name', // non-existent field
+ 'bffs.0.crush', // non-existent field
+ ];
+
+ let omitted = new Omitter(values, jsonFields).omit([
+ 'last_name',
+ 'middle_name', // non-existent field
+ 'bffs.0.name',
+ 'bffs.0.name', // duplicate field
+ 'bffs.1.name', // non-existent field
+ 'bffs.0.crush', // non-existent field
+ 'bffs.0.crush.0.name', // non-existent field
+ ]);
+
+ let expected = {
+ first_name: 'Han',
+ bffs: JSON.stringify([
+ {
+ type: 'Wookie',
+ },
+ ]),
+ };
+
+ expect(omitted).toEqual(expected);
+});
diff --git a/src/Fields/Field.php b/src/Fields/Field.php
index fc756cb723..01c4335475 100644
--- a/src/Fields/Field.php
+++ b/src/Fields/Field.php
@@ -19,7 +19,6 @@ class Field implements Arrayable
protected $value;
protected $parent;
protected $parentField;
- protected $filled = false;
protected $validationContext;
public function __construct($handle, array $config)
@@ -33,8 +32,7 @@ public function newInstance()
return (new static($this->handle, $this->config))
->setParent($this->parent)
->setParentField($this->parentField)
- ->setValue($this->value)
- ->setFilled($this->filled);
+ ->setValue($this->value);
}
public function setHandle(string $handle)
@@ -201,11 +199,6 @@ public function isFilterable()
return (bool) $this->get('filterable');
}
- public function isFilled()
- {
- return (bool) $this->filled;
- }
-
public function toPublishArray()
{
return array_merge($this->preProcessedConfig(), [
@@ -232,13 +225,6 @@ public function toBlueprintArray()
];
}
- public function setFilled($filled)
- {
- $this->filled = $filled;
-
- return $this;
- }
-
public function setValue($value)
{
$this->value = $value;
@@ -246,14 +232,6 @@ public function setValue($value)
return $this;
}
- public function fillValue($value)
- {
- $this->value = $value;
- $this->filled = true;
-
- return $this;
- }
-
public function value()
{
return $this->value;
diff --git a/src/Fields/Fields.php b/src/Fields/Fields.php
index ba494006ed..7607960ecd 100644
--- a/src/Fields/Fields.php
+++ b/src/Fields/Fields.php
@@ -15,6 +15,8 @@ class Fields
protected $fields;
protected $parent;
protected $parentField;
+ protected $filled = [];
+ protected $withValidatableValues = false;
public function __construct($items = [], $parent = null, $parentField = null)
{
@@ -58,6 +60,20 @@ public function setParentField($field)
return $this;
}
+ public function setFilled($dottedKeys)
+ {
+ $this->filled = $dottedKeys;
+
+ return $this;
+ }
+
+ public function withValidatableValues()
+ {
+ $this->withValidatableValues = true;
+
+ return $this;
+ }
+
public function items()
{
return $this->items;
@@ -84,7 +100,8 @@ public function newInstance()
->setParent($this->parent)
->setParentField($this->parentField)
->setItems($this->items)
- ->setFields($this->fields);
+ ->setFields($this->fields)
+ ->setFilled($this->filled);
}
public function localizable()
@@ -118,27 +135,28 @@ public function toPublishArray()
public function addValues(array $values)
{
+ $filled = array_keys($values);
+
$fields = $this->fields->map(function ($field) use ($values) {
- return Arr::has($values, $field->handle())
- ? $field->newInstance()->fillValue(Arr::get($values, $field->handle()))
- : $field->newInstance();
+ return $field->newInstance()->setValue(Arr::get($values, $field->handle()));
});
- return $this->newInstance()->setFields($fields);
+ return $this->newInstance()->setFilled($filled)->setFields($fields);
}
public function values()
{
- return $this->fields->mapWithKeys(function ($field) {
+ $values = $this->fields->mapWithKeys(function ($field) {
return [$field->handle() => $field->value()];
});
- }
- public function validatableValues()
- {
- return $this->values()->filter(function ($value, $handle) {
- return $this->fields->get($handle)->isFilled();
- });
+ if ($this->withValidatableValues) {
+ $values = $values->filter(function ($field, $handle) {
+ return in_array($handle, $this->filled);
+ });
+ }
+
+ return $values;
}
public function process()
@@ -159,7 +177,7 @@ public function preProcessValidatables()
{
return $this->newInstance()->setFields(
$this->fields->map->preProcessValidatable()
- );
+ )->withValidatableValues();
}
public function augment()
diff --git a/src/Fields/Validator.php b/src/Fields/Validator.php
index 7f2e1267c4..2e4a4f6e5f 100644
--- a/src/Fields/Validator.php
+++ b/src/Fields/Validator.php
@@ -96,7 +96,7 @@ public function withReplacements($replacements)
public function validate()
{
return LaravelValidator::validate(
- $this->fields->preProcessValidatables()->validatableValues()->all(),
+ $this->fields->preProcessValidatables()->values()->all(),
$this->rules(),
$this->customMessages,
$this->attributes()
diff --git a/src/Fieldtypes/Grid.php b/src/Fieldtypes/Grid.php
index 5b98b6db20..9a08f86ecc 100644
--- a/src/Fieldtypes/Grid.php
+++ b/src/Fieldtypes/Grid.php
@@ -163,8 +163,8 @@ public function extraValidationAttributes(): array
$attributes = $this->fields()->validator()->attributes();
return collect($this->field->value())->map(function ($row, $index) use ($attributes) {
- return collect($row)->except('_id')->mapWithKeys(function ($value, $handle) use ($attributes, $index) {
- return [$this->rowRuleFieldPrefix($index).'.'.$handle => $attributes[$handle] ?? null];
+ return collect($attributes)->except('_id')->mapWithKeys(function ($attribute, $handle) use ($index) {
+ return [$this->rowRuleFieldPrefix($index).'.'.$handle => $attribute];
});
})->reduce(function ($carry, $rules) {
return $carry->merge($rules);
diff --git a/tests/Fields/FieldsTest.php b/tests/Fields/FieldsTest.php
index 2a97950929..622b2a6981 100644
--- a/tests/Fields/FieldsTest.php
+++ b/tests/Fields/FieldsTest.php
@@ -536,29 +536,67 @@ public function it_adds_values_to_fields()
}
/** @test */
- public function adding_values_sets_filled_status_on_fields_for_validation()
+ public function preprocessing_validatables_removes_unfilled_values()
{
- FieldRepository::shouldReceive('find')->with('one')->andReturnUsing(function () {
- return new Field('one', []);
- });
-
- FieldRepository::shouldReceive('find')->with('two')->andReturnUsing(function () {
- return new Field('two', []);
- });
-
$fields = new Fields([
- ['handle' => 'one', 'field' => 'one'],
- ['handle' => 'two', 'field' => 'two'],
+ ['handle' => 'title', 'field' => ['type' => 'text']],
+ ['handle' => 'one', 'field' => ['type' => 'text']],
+ ['handle' => 'two', 'field' => ['type' => 'text']],
+ ['handle' => 'reppy', 'field' => ['type' => 'replicator', 'sets' => ['replicator_set' => ['fields' => [
+ ['handle' => 'title', 'field' => ['type' => 'text']],
+ ['handle' => 'one', 'field' => ['type' => 'text']],
+ ['handle' => 'two', 'field' => ['type' => 'text']],
+ ['handle' => 'griddy_in_reppy', 'field' => ['type' => 'grid', 'fields' => [
+ ['handle' => 'title', 'field' => ['type' => 'text']],
+ ['handle' => 'one', 'field' => ['type' => 'text']],
+ ['handle' => 'two', 'field' => ['type' => 'text']],
+ ['handle' => 'bardo_in_griddy_in_reppy', 'field' => ['type' => 'bard', 'sets' => ['bard_set' => ['fields' => [
+ ['handle' => 'title', 'field' => ['type' => 'text']],
+ ['handle' => 'one', 'field' => ['type' => 'text']],
+ ['handle' => 'two', 'field' => ['type' => 'text']],
+ ['handle' => 'bardo_in_bardo_in_griddy_in_reppy', 'field' => ['type' => 'bard', 'sets' => ['bard_set_set' => ['fields' => [
+ ['handle' => 'title', 'field' => ['type' => 'text']],
+ ['handle' => 'one', 'field' => ['type' => 'text']],
+ ['handle' => 'two', 'field' => ['type' => 'text']],
+ ]]]]],
+ ]]]]],
+ ]]],
+ ]]]]],
]);
- $this->assertEquals(['one' => null, 'two' => null], $fields->values()->all());
+ $this->assertEquals(['title' => null, 'one' => null, 'two' => null, 'reppy' => null], $fields->values()->all());
+ $this->assertEquals([], $fields->preProcessValidatables()->values()->all());
+
+ $values = $expected = [
+ 'title' => 'recursion madness',
+ 'one' => 'foo',
+ 'reppy' => [
+ ['type' => 'replicator_set', 'two' => 'foo'],
+ ['type' => 'replicator_set', 'griddy_in_reppy' => [
+ ['one' => 'foo'],
+ ['bardo_in_griddy_in_reppy' => json_encode($bardValues = [
+ ['type' => 'set', 'attrs' => ['values' => ['type' => 'bard_set', 'two' => 'foo']]],
+ ['type' => 'paragraph', 'content' => [['type' => 'text', 'text' => 'foo']]],
+ ['type' => 'set', 'attrs' => ['type' => 'bard_set', 'values' => ['type' => 'bard_set', 'bardo_in_bardo_in_griddy_in_reppy' => json_encode($doubleNestedBardValues = [
+ ['type' => 'set', 'attrs' => ['values' => ['type' => 'bard_set', 'two' => 'foo']]],
+ ['type' => 'paragraph', 'content' => [['type' => 'text', 'text' => 'foo']]],
+ ])]]],
+ ])],
+ ]],
+ ],
+ ];
+
+ // Calling `addValues()` should track which fields are filled at each level of nesting.
+ // When we call `preProcessValidatables()`, unfilled values should get removed, in
+ // order to ensure rules like `sometimes` and `required_if` work at all levels.
+ $validatableValues = $fields->addValues($values)->preProcessValidatables()->values()->all();
- $fields = $fields->addValues(['one' => 'foo']);
+ // Bard fields submit JSON values, so we'll replace them with their corresponding PHP array
+ // values here, since `preProcessValidatables()` will return JSON decoded decoded values.
+ $expected['reppy'][1]['griddy_in_reppy'][1]['bardo_in_griddy_in_reppy'] = $bardValues;
+ $expected['reppy'][1]['griddy_in_reppy'][1]['bardo_in_griddy_in_reppy'][2]['attrs']['values']['bardo_in_bardo_in_griddy_in_reppy'] = $doubleNestedBardValues;
- $this->assertTrue($fields->get('one')->isFilled());
- $this->assertFalse($fields->get('two')->isFilled());
- $this->assertEquals(['one' => 'foo', 'two' => null], $fields->values()->all());
- $this->assertEquals(['one' => 'foo'], $fields->validatableValues()->all());
+ $this->assertEquals($expected, $validatableValues);
}
/** @test */
diff --git a/tests/Fields/ValidatorTest.php b/tests/Fields/ValidatorTest.php
index 707ac0c34f..3eb0684a15 100644
--- a/tests/Fields/ValidatorTest.php
+++ b/tests/Fields/ValidatorTest.php
@@ -328,7 +328,9 @@ public function it_replaces_this_in_sets()
['type' => 'replicator_set', 'nested_replicator' => [['type' => 'replicator_set', 'nested_replicator' => [['type' => 'replicator_set']]]]],
],
'replicator_with_nested_grid' => [
- ['type' => 'replicator_set', 'nested_grid' => [[]]],
+ ['type' => 'replicator_set', 'nested_grid' => [
+ ['text' => null, 'not_in_blueprint' => 'test'],
+ ]],
],
'replicator_with_nested_bard' => [
['type' => 'replicator_set', 'nested_bard' => [['type' => 'set', 'attrs' => ['values' => ['type' => 'bard_set']]]]],