Skip to content

Commit 32bda02

Browse files
fix: prevent crash on withoutUserOptions fields and allow operator overrides (#103)
- Skip option normalization in BackendVisibilityService for fields with withoutUserOptions (e.g. repeater), fixing array_key_exists TypeError - Add visibilityOperators() to FieldSchema/FieldTypeData so field types can declare their own compatible visibility operators - Route all operator resolution through FieldTypeData instead of FieldDataType directly (4 call sites)
1 parent a9a882a commit 32bda02

7 files changed

Lines changed: 129 additions & 6 deletions

File tree

src/Data/FieldTypeData.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Closure;
88
use Relaticle\CustomFields\Enums\FieldDataType;
9+
use Relaticle\CustomFields\Enums\VisibilityOperator;
910
use Spatie\LaravelData\Data;
1011
use Stringable;
1112

@@ -30,8 +31,28 @@ public function __construct(
3031
public array $validationRules = [],
3132
public ?string $settingsDataClass = null,
3233
public string|Closure|null $settingsSchema = null,
34+
/** @var array<int, VisibilityOperator>|null */
35+
public ?array $visibilityOperators = null,
3336
) {}
3437

38+
/**
39+
* @return array<int, VisibilityOperator>
40+
*/
41+
public function getCompatibleOperators(): array
42+
{
43+
return $this->visibilityOperators ?? $this->dataType->getCompatibleOperators();
44+
}
45+
46+
/**
47+
* @return array<string, string>
48+
*/
49+
public function getCompatibleOperatorOptions(): array
50+
{
51+
return collect($this->getCompatibleOperators())
52+
->mapWithKeys(fn (VisibilityOperator $operator): array => [$operator->value => $operator->getLabel()])
53+
->toArray();
54+
}
55+
3556
public function __toString(): string
3657
{
3758
return $this->key;

src/Enums/VisibilityOperator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,6 @@ public static function forFieldType(string $fieldType): array
176176
// For string field types, use the new field type system
177177
$fieldTypeData = CustomFieldsType::getFieldType($fieldType);
178178

179-
return $fieldTypeData->dataType->getCompatibleOperatorOptions();
179+
return $fieldTypeData->getCompatibleOperatorOptions();
180180
}
181181
}

src/FieldTypeSystem/FieldSchema.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ class FieldSchema
5555

5656
protected bool $withoutUserOptions = false;
5757

58+
/** @var array<int, \Relaticle\CustomFields\Enums\VisibilityOperator>|null */
59+
private ?array $visibilityOperators = null;
60+
5861
private ?string $settingsDataClass = null;
5962

6063
private string|Closure|null $settingsSchema = null;
@@ -362,6 +365,18 @@ public function withoutUserOptions(): self
362365
return $this;
363366
}
364367

368+
/**
369+
* Override the default visibility operators derived from the data type.
370+
*
371+
* @param array<int, \Relaticle\CustomFields\Enums\VisibilityOperator> $operators
372+
*/
373+
public function visibilityOperators(array $operators): self
374+
{
375+
$this->visibilityOperators = $operators;
376+
377+
return $this;
378+
}
379+
365380
// ========== Export Configuration ==========
366381

367382
/**
@@ -554,7 +569,8 @@ public function data(): FieldTypeData
554569
acceptsArbitraryValues: $this->acceptsArbitraryValues,
555570
validationRules: $this->availableValidationRules,
556571
settingsDataClass: $this->settingsDataClass,
557-
settingsSchema: $this->settingsSchema
572+
settingsSchema: $this->settingsSchema,
573+
visibilityOperators: $this->visibilityOperators
558574
);
559575
}
560576
}

src/Filament/Management/Forms/Components/VisibilityComponent.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ private function getCompatibleOperators(Get $get): array
322322
$fieldData = $this->getFieldTypeData($get);
323323

324324
return $fieldData
325-
? $fieldData->dataType->getCompatibleOperatorOptions()
325+
? $fieldData->getCompatibleOperatorOptions()
326326
: VisibilityOperator::options();
327327
}
328328

src/Services/Visibility/BackendVisibilityService.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ private function normalizeValueForEvaluation(
166166
if (
167167
$value === null ||
168168
$value === '' ||
169-
! $field->isChoiceField()
169+
! $field->isChoiceField() ||
170+
$field->typeData->withoutUserOptions
170171
) {
171172
return $value;
172173
}

src/Services/Visibility/CoreVisibilityLogicService.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ public function isOperatorCompatible(VisibilityOperator $operator, CustomField $
201201
return false;
202202
}
203203

204-
$compatibleOperators = $typeData->dataType->getCompatibleOperators();
204+
$compatibleOperators = $typeData->getCompatibleOperators();
205205

206206
return in_array($operator, $compatibleOperators, true);
207207
}
@@ -239,7 +239,7 @@ public function getFieldMetadata(CustomField $field): array
239239
'category' => $typeData->dataType->value,
240240
'is_optionable' => $typeData->dataType->isChoiceField(),
241241
'has_multiple_values' => $typeData->dataType->isMultiChoiceField(),
242-
'compatible_operators' => $typeData->dataType->getCompatibleOperators(),
242+
'compatible_operators' => $typeData->getCompatibleOperators(),
243243
'has_visibility_conditions' => $this->hasVisibilityConditions($field),
244244
'visibility_mode' => $this->getVisibilityMode($field)->value,
245245
'visibility_logic' => $this->getVisibilityLogic($field)->value,

tests/Feature/UnifiedVisibilityConsistencyTest.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66
use Relaticle\CustomFields\Enums\VisibilityLogic;
77
use Relaticle\CustomFields\Enums\VisibilityMode;
88
use Relaticle\CustomFields\Enums\VisibilityOperator;
9+
use Relaticle\CustomFields\Facades\CustomFieldsType;
10+
use Relaticle\CustomFields\FieldTypeSystem\BaseFieldType;
11+
use Relaticle\CustomFields\FieldTypeSystem\FieldSchema;
912
use Relaticle\CustomFields\Models\CustomField;
1013
use Relaticle\CustomFields\Models\CustomFieldSection;
1114
use Relaticle\CustomFields\Services\Visibility\BackendVisibilityService;
1215
use Relaticle\CustomFields\Services\Visibility\CoreVisibilityLogicService;
1316
use Relaticle\CustomFields\Services\Visibility\FrontendVisibilityService;
17+
use Relaticle\CustomFields\Tests\Fixtures\Models\Post;
1418
use Relaticle\CustomFields\Tests\Fixtures\Models\User;
1519

1620
beforeEach(function (): void {
@@ -318,3 +322,84 @@
318322
$jsExpression = $this->frontendService->buildVisibilityExpression($this->conditionalField, $fields);
319323
expect($jsExpression)->toBeString(); // Should generate valid expression even with null comparison
320324
});
325+
326+
test('withoutUserOptions multi-choice fields do not crash during value extraction', function (): void {
327+
CustomFieldsType::register([
328+
'test-repeater' => TestRepeaterFieldType::class,
329+
]);
330+
331+
$section = CustomFieldSection::factory()->create([
332+
'name' => 'Repeater Section',
333+
'entity_type' => Post::class,
334+
'active' => true,
335+
]);
336+
337+
$repeaterField = CustomField::factory()->create([
338+
'custom_field_section_id' => $section->id,
339+
'name' => 'Repeater',
340+
'code' => 'repeater',
341+
'type' => 'test-repeater',
342+
'active' => true,
343+
]);
344+
345+
$post = Post::factory()->create();
346+
$post->saveCustomFieldValue($repeaterField, [
347+
['value' => 'item 1'],
348+
['value' => 'item 2'],
349+
]);
350+
$post->load('customFieldValues.customField');
351+
352+
$backendService = app(BackendVisibilityService::class);
353+
354+
$result = $backendService->extractFieldValues($post, collect([$repeaterField]));
355+
356+
expect($result)
357+
->toHaveKey('repeater')
358+
->and($result['repeater'])->toBe([
359+
['value' => 'item 1'],
360+
['value' => 'item 2'],
361+
]);
362+
363+
$visibleFields = $backendService->getVisibleFields($post, collect([$repeaterField]));
364+
365+
expect($visibleFields)->toHaveCount(1);
366+
});
367+
368+
test('field types can override compatible visibility operators', function (): void {
369+
CustomFieldsType::register([
370+
'test-repeater' => TestRepeaterFieldType::class,
371+
]);
372+
373+
$fieldTypeData = CustomFieldsType::getFieldType('test-repeater');
374+
375+
expect($fieldTypeData->getCompatibleOperators())->toBe([
376+
VisibilityOperator::IS_EMPTY,
377+
VisibilityOperator::IS_NOT_EMPTY,
378+
]);
379+
});
380+
381+
test('field types without operator override use dataType defaults', function (): void {
382+
$fieldTypeData = CustomFieldsType::getFieldType('select');
383+
384+
expect($fieldTypeData->getCompatibleOperators())
385+
->toBe($fieldTypeData->dataType->getCompatibleOperators());
386+
});
387+
388+
class TestRepeaterFieldType extends BaseFieldType
389+
{
390+
public function configure(): FieldSchema
391+
{
392+
return FieldSchema::multiChoice()
393+
->key('test-repeater')
394+
->label('Test Repeater')
395+
->icon('heroicon-o-squares-plus')
396+
->searchable(false)
397+
->sortable(false)
398+
->filterable(false)
399+
->withoutUserOptions()
400+
->visibilityOperators([
401+
VisibilityOperator::IS_EMPTY,
402+
VisibilityOperator::IS_NOT_EMPTY,
403+
]);
404+
}
405+
}

0 commit comments

Comments
 (0)