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
15 changes: 13 additions & 2 deletions src/Livewire/ModuleTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ public function saveModal(): void
return;
}

$this->validate($this->modalRules(), [], $this->modalValidationAttributes());
$this->validateModalData();

if (($method = $this->modalConfig('save_provider')) && method_exists($this->provider(), $method)) {
$savedId = $this->callProvider((string) $method, $this->modalData, $this->modalRecordId, $this->modalMode);
Expand Down Expand Up @@ -292,7 +292,7 @@ public function saveModalAction(string $key): void
data_set($this->modalData, (string) $field, $value);
}

$this->validate($this->modalRules(), [], $this->modalValidationAttributes());
$this->validateModalData();
$method = (string) ($action['save_provider'] ?? $this->modalConfig('save_provider', 'saveModal'));

if (!method_exists($this->provider(), $method)) {
Expand Down Expand Up @@ -1574,6 +1574,17 @@ protected function modalRules(): array
return $rules;
}

protected function validateModalData(): void
{
$rules = $this->modalRules();

if ($rules === []) {
return;
}

$this->validate($rules, [], $this->modalValidationAttributes());
}

/** @return array<string, string> */
protected function modalValidationAttributes(): array
{
Expand Down
16 changes: 16 additions & 0 deletions tests/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -1543,6 +1543,16 @@ function evo_ui_config(string $path): array
evo_ui_assert_contains('$withActionKey', $list, 'List row actions must support action-key arguments like table rows.');
});

evo_ui_test('module modal skips Livewire validation when no field rules are configured', function (): void {
$moduleTable = evo_ui_read('src/Livewire/ModuleTable.php');

evo_ui_assert_contains('protected function validateModalData(): void', $moduleTable, 'ModuleTable must centralize modal validation.');
evo_ui_assert_contains('$rules = $this->modalRules();', $moduleTable, 'ModuleTable modal validation must read configured field rules.');
evo_ui_assert_contains('if ($rules === []) {' . PHP_EOL . ' return;' . PHP_EOL . ' }', $moduleTable, 'ModuleTable must skip Livewire validation when modal rules are empty.');
evo_ui_assert_contains('$this->validate($rules, [], $this->modalValidationAttributes());', $moduleTable, 'ModuleTable must still validate when modal rules exist.');
evo_ui_assert_not_contains('$this->validate($this->modalRules(), [], $this->modalValidationAttributes());', $moduleTable, 'ModuleTable must not call Livewire validation with an empty rules array.');
});

evo_ui_test('module table exposes filtering, sorting, pagination and view state methods', function (): void {
$moduleTable = evo_ui_read('src/Livewire/ModuleTable.php');
$moduleTableView = evo_ui_read('views/components/table/module.blade.php');
Expand Down Expand Up @@ -2119,6 +2129,12 @@ function evo_ui_config(string $path): array
'$modalNotices' => 'Modal form must support configured notice blocks.',
'evo-ui-modal__notices' => 'Modal notices must render in a dedicated container.',
'placeholder="{{ $placeholder }}"' => 'Top-level modal fields must render configured placeholders.',
'visible_if_all' => 'Modal fields must support multiple visibility conditions.',
"->implode(' && ')" => 'Multiple visibility conditions must be combined with AND logic.',
'visible_if_any' => 'Modal fields must support alternative visibility conditions.',
"->implode(' || ')" => 'Alternative visibility conditions must be combined with OR logic.',
'addon_prefix' => 'Modal text fields must support input add-on prefixes.',
'evo-ui-input-group__addon' => 'Modal input add-ons must render a visible prefix.',
'EvoUI.browseImageField' => 'Image fields must open through the shared media bridge.',
'EvoUI.browseMediaField' => 'File fields must open through the shared media bridge.',
'EvoUI.syncRichEditors($el, $wire).then(() => $wire.saveModal())' => 'Modal submit must sync editors before save.',
Expand Down
48 changes: 45 additions & 3 deletions views/components/table/module/modal-field.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
$help = $field['help'] ?? $field['description'] ?? null;
$helpText = $help ? __((string) $help) : '';
$placeholder = !empty($field['placeholder']) ? __((string) $field['placeholder']) : '';
$addonPrefix = (string) ($field['addon_prefix'] ?? '');
$fieldIdSuffix = (string) ($field['id_suffix'] ?? '');
$fieldId = 'evo-modal-' . preg_replace('/[^a-z0-9_-]/i', '-', trim($controller->preset . '-' . $name . '-' . $fieldIdSuffix, '-'));
$section = preg_replace('/[^a-z0-9_-]/i', '-', (string) ($field['section'] ?? ''));
Expand All @@ -22,6 +23,12 @@
$gridRow = trim((string) ($field['grid_row'] ?? ''));
$fieldStyle = preg_match('/^\d+(?:\s*\/\s*\d+)?$/', $gridRow) ? 'grid-row: ' . $gridRow . ';' : '';
$visibleIf = is_array($field['visible_if'] ?? null) ? $field['visible_if'] : [];
$visibleIfAll = collect((array) ($field['visible_if_all'] ?? []))
->filter(fn ($condition) => is_array($condition) && !empty($condition['field']))
->values();
$visibleIfAny = collect((array) ($field['visible_if_any'] ?? []))
->filter(fn ($condition) => is_array($condition) && !empty($condition['field']))
->values();
$visibleIfField = (string) ($visibleIf['field'] ?? '');
$visibleIfExpected = $visibleIf['value'] ?? true;
$visibilityExpression = '';
Expand All @@ -41,6 +48,32 @@
: $fieldExpression;
}

if ($visibleIfAll->isNotEmpty()) {
$fieldExpression = $visibleIfAll
->map(fn ($condition) => 'fieldVisible('
. json_encode((string) ($condition['field'] ?? ''), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
. ', '
. json_encode($condition['value'] ?? true, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
. ')')
->implode(' && ');
$visibilityExpression = $visibilityExpression !== ''
? '(' . $visibilityExpression . ') && (' . $fieldExpression . ')'
: '(' . $fieldExpression . ')';
}

if ($visibleIfAny->isNotEmpty()) {
$fieldExpression = $visibleIfAny
->map(fn ($condition) => 'fieldVisible('
. json_encode((string) ($condition['field'] ?? ''), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
. ', '
. json_encode($condition['value'] ?? true, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
. ')')
->implode(' || ');
$visibilityExpression = $visibilityExpression !== ''
? '(' . $visibilityExpression . ') && (' . $fieldExpression . ')'
: '(' . $fieldExpression . ')';
}

$colorFallback = (string) ($field['default'] ?? '#64748B');
$colorFallback = preg_match('/^#[0-9a-f]{6}$/i', $colorFallback) ? strtoupper($colorFallback) : '#64748B';
@endphp
Expand Down Expand Up @@ -912,10 +945,19 @@ class="evo-ui-textarea evo-ui-textarea--editor"
@php
$inputType = in_array($type, ['email', 'number', 'date', 'datetime-local'], true) ? $type : 'text';
@endphp
@if(!empty($field['live']))
<input id="{{ $fieldId }}" type="{{ $inputType }}" class="evo-ui-input" wire:model.live.debounce.350ms="{{ $model }}" autocomplete="off" placeholder="{{ $placeholder }}" @if($inputType === 'number') min="{{ (int) ($field['min'] ?? 0) }}" @endif>
@if($addonPrefix !== '')
<span class="evo-ui-input-group" style="display: flex; width: 100%;">
<span class="evo-ui-input-group__addon" style="display: inline-flex; align-items: center; padding: 0 0.875rem; border: 1px solid var(--evo-ui-border); border-right: 0; border-radius: var(--evo-ui-radius-sm) 0 0 var(--evo-ui-radius-sm); background: color-mix(in oklch, var(--evo-ui-muted) 9%, transparent); color: var(--evo-ui-muted);">{{ $addonPrefix }}</span>
@if(!empty($field['live']))
<input id="{{ $fieldId }}" type="{{ $inputType }}" class="evo-ui-input" style="flex: 1; min-width: 0; border-top-left-radius: 0; border-bottom-left-radius: 0;" wire:model.live.debounce.350ms="{{ $model }}" autocomplete="off" placeholder="{{ $placeholder }}" @if($inputType === 'number') min="{{ (int) ($field['min'] ?? 0) }}" @if(array_key_exists('max', $field)) max="{{ (int) $field['max'] }}" @endif @endif>
@else
<input id="{{ $fieldId }}" type="{{ $inputType }}" class="evo-ui-input" style="flex: 1; min-width: 0; border-top-left-radius: 0; border-bottom-left-radius: 0;" wire:model.blur="{{ $model }}" autocomplete="off" placeholder="{{ $placeholder }}" @if($inputType === 'number') min="{{ (int) ($field['min'] ?? 0) }}" @if(array_key_exists('max', $field)) max="{{ (int) $field['max'] }}" @endif @endif>
@endif
</span>
@elseif(!empty($field['live']))
<input id="{{ $fieldId }}" type="{{ $inputType }}" class="evo-ui-input" wire:model.live.debounce.350ms="{{ $model }}" autocomplete="off" placeholder="{{ $placeholder }}" @if($inputType === 'number') min="{{ (int) ($field['min'] ?? 0) }}" @if(array_key_exists('max', $field)) max="{{ (int) $field['max'] }}" @endif @endif>
@else
<input id="{{ $fieldId }}" type="{{ $inputType }}" class="evo-ui-input" wire:model.blur="{{ $model }}" autocomplete="off" placeholder="{{ $placeholder }}" @if($inputType === 'number') min="{{ (int) ($field['min'] ?? 0) }}" @endif>
<input id="{{ $fieldId }}" type="{{ $inputType }}" class="evo-ui-input" wire:model.blur="{{ $model }}" autocomplete="off" placeholder="{{ $placeholder }}" @if($inputType === 'number') min="{{ (int) ($field['min'] ?? 0) }}" @if(array_key_exists('max', $field)) max="{{ (int) $field['max'] }}" @endif @endif>
@endif
@endif

Expand Down