From 3cf840391f0f12d0b87e7492dad8075b2210f2c0 Mon Sep 17 00:00:00 2001 From: Rias Date: Mon, 21 Sep 2026 17:01:30 +0200 Subject: [PATCH 1/3] Add default row values to table fields Amp-Thread-ID: https://ampcode.com/threads/T-01a0c462-60db-779c-8896-bc9e3e4076f9 --- .../js/modules/forms/FormRenderer.test.ts | 22 +++++++++-- resources/js/modules/forms/TableControl.vue | 2 + src/Field/Table.php | 37 +++++++++++++++++-- src/Form/Controls/Table.php | 13 +++++++ tests/Unit/Field/TableTest.php | 31 ++++++++++++++++ 5 files changed, 97 insertions(+), 8 deletions(-) diff --git a/resources/js/modules/forms/FormRenderer.test.ts b/resources/js/modules/forms/FormRenderer.test.ts index 2bfd4730a8a..54974a8805d 100644 --- a/resources/js/modules/forms/FormRenderer.test.ts +++ b/resources/js/modules/forms/FormRenderer.test.ts @@ -270,7 +270,10 @@ vi.mock('../editable-table', () => ({ string, {type: string; textExpanderTriggers?: TextExpanderTriggers} >, - settings: {minRows?: number | null} = {} + settings: { + defaultValues?: FormValues; + minRows?: number | null; + } = {} ) { const body = required( document.querySelector(`#${id} tbody`), @@ -282,7 +285,7 @@ vi.mock('../editable-table', () => ({ String(body.children.length), columns, baseName, - {} + settings.defaultValues ?? {} ).appendTo(body); } } @@ -2946,6 +2949,7 @@ describe('FormRenderer', () => { allowDelete: true, allowReorder: true, minRows: 2, + defaultValues: {name: '', enabled: false}, }, [{name: '', enabled: true}], ], @@ -3097,6 +3101,16 @@ describe('FormRenderer', () => { 'input[name="settings[rows][0][enabled]"][type="checkbox"]' )?.checked ).toBe(true); + expect( + container.querySelector( + 'textarea[name="settings[rows][1][name]"]' + )?.value + ).toBe(''); + expect( + container.querySelector( + 'input[name="settings[rows][1][enabled]"][type="checkbox"]' + )?.checked + ).toBe(false); await vi.waitFor(() => expect( container.querySelector( @@ -3136,7 +3150,7 @@ describe('FormRenderer', () => { )?.value ).toEqual([ {name: '', enabled: true}, - {name: '', enabled: false}, + {name: '', enabled: false}, ]) ); @@ -3172,7 +3186,7 @@ describe('FormRenderer', () => { settings: { rows: [ {name: '', enabled: true}, - {name: '', enabled: false}, + {name: '', enabled: false}, ], }, }); diff --git a/resources/js/modules/forms/TableControl.vue b/resources/js/modules/forms/TableControl.vue index cb31004f829..3a936597093 100644 --- a/resources/js/modules/forms/TableControl.vue +++ b/resources/js/modules/forms/TableControl.vue @@ -13,6 +13,7 @@ type TableControlProps = { columns: EditableTableColumns; + defaultValues?: EditableTableRow; allowAdd?: boolean; allowDelete?: boolean; allowReorder?: boolean; @@ -122,6 +123,7 @@ } instance = new EditableTable(id, name, props.control.props.columns, { + defaultValues: props.control.props.defaultValues, allowAdd: props.control.props.allowAdd, allowDelete: props.control.props.allowDelete, allowReorder: props.control.props.allowReorder, diff --git a/src/Field/Table.php b/src/Field/Table.php index c81bfaaf56a..ed0a10fc974 100644 --- a/src/Field/Table.php +++ b/src/Field/Table.php @@ -121,6 +121,7 @@ public function formControl(FieldContext $context): Control return TableControl::make($context->path) ->columns($columns) + ->defaultValues($this->defaultRowValues) ->allowAdd(! $this->staticRows) ->allowDelete(! $this->staticRows) ->allowReorder(! $this->staticRows) @@ -174,6 +175,13 @@ public function settingsForm(FormContext $context = new FormContext): Form ->allowDelete() ->allowReorder() ->value($this->defaults ?? [])), + FormField::make(t('Default Row Values')) + ->instructions(t('Define the default values for new rows.')) + ->control(TableControl::make('defaultRowValues') + ->columns($defaultColumns) + ->minRows(1) + ->maxRows(1) + ->value([$this->defaultRowValues])), ])->dependsOn('settings.columns'), FormField::make(t('Static Rows')) ->instructions(t('Whether the table rows should be restricted to those defined by the “Default Values” setting.')) @@ -222,6 +230,9 @@ public function settingsForm(FormContext $context = new FormContext): Form /** @var list|null The default row values that new elements should have */ public ?array $defaults = [[]]; + /** @var TableRowData The default values for newly added rows */ + public array $defaultRowValues = []; + public function __construct($config = []) { // Config normalization @@ -260,15 +271,32 @@ public function __construct($config = []) } } + if (isset($config['defaultRowValues'])) { + $defaultRowValues = $config['defaultRowValues']; + + $config['defaultRowValues'] = match (true) { + ! is_array($defaultRowValues) => [], + count($defaultRowValues) === 1 + && is_array($defaultRowValues[0] ?? null) => $defaultRowValues[0], + default => $defaultRowValues, + }; + } + // handle some default cell values - if (! empty($config['columns']) && isset($config['defaults'])) { + if (! empty($config['columns'])) { foreach ($config['columns'] as $colId => $col) { // Convert default date cell values to ISO8601 strings if (in_array($col['type'], ['date', 'time'], true)) { - foreach ($config['defaults'] as &$row) { - if (isset($row[$colId])) { - $row[$colId] = DateTimeHelper::toIso8601($row[$colId]) ?: null; + if (isset($config['defaults'])) { + foreach ($config['defaults'] as &$row) { + if (isset($row[$colId])) { + $row[$colId] = DateTimeHelper::toIso8601($row[$colId]) ?: null; + } } + unset($row); + } + if (isset($config['defaultRowValues'][$colId])) { + $config['defaultRowValues'][$colId] = DateTimeHelper::toIso8601($config['defaultRowValues'][$colId]) ?: null; } } } @@ -842,6 +870,7 @@ private function inlineInputHtml(mixed $value, ?ElementInterface $element): stri 'name' => $this->handle, 'cols' => $columns, 'rows' => $value, + 'defaultValues' => $this->defaultRowValues, 'minRows' => $this->minRows, 'maxRows' => $this->maxRows, 'static' => false, diff --git a/src/Form/Controls/Table.php b/src/Form/Controls/Table.php index c6bea10285a..03c09887371 100644 --- a/src/Form/Controls/Table.php +++ b/src/Form/Controls/Table.php @@ -19,6 +19,9 @@ class Table extends Control /** @var array> */ private array $columns = []; + /** @var array */ + private array $defaultValues = []; + private bool $allowAdd = false; private bool $allowDelete = false; @@ -45,6 +48,7 @@ public static function renderHtml(ControlPayload $control, mixed $value, array $ 'name' => $attributes['name'], 'cols' => $control->props['columns'], 'rows' => $rows, + 'defaultValues' => $control->props['defaultValues'] ?? [], 'allowAdd' => (bool) ($control->props['allowAdd'] ?? false), 'allowDelete' => (bool) ($control->props['allowDelete'] ?? false), 'allowReorder' => (bool) ($control->props['allowReorder'] ?? false), @@ -68,6 +72,14 @@ public function columns(array $columns): static return $this; } + /** @param array $defaultValues */ + public function defaultValues(array $defaultValues): static + { + $this->defaultValues = $defaultValues; + + return $this; + } + public function allowAdd(bool $allowAdd = true): static { $this->allowAdd = $allowAdd; @@ -130,6 +142,7 @@ public function props(mixed $value = null): array { return Arr::whereNotNull([ 'columns' => $this->columns, + 'defaultValues' => $this->defaultValues, 'allowAdd' => $this->allowAdd, 'allowDelete' => $this->allowDelete, 'allowReorder' => $this->allowReorder, diff --git a/tests/Unit/Field/TableTest.php b/tests/Unit/Field/TableTest.php index 9ce641f5435..6a32d3714fa 100644 --- a/tests/Unit/Field/TableTest.php +++ b/tests/Unit/Field/TableTest.php @@ -2,12 +2,43 @@ declare(strict_types=1); +use CraftCms\Cms\Field\FieldContext; use CraftCms\Cms\Field\Table; use CraftCms\Cms\Form\FormContext; use CraftCms\Cms\Form\FormHtmlRenderer; use CraftCms\Cms\Form\FormResolver; use Symfony\Component\DomCrawler\Crawler; +it('uses configured default row values for newly added rows', function () { + $field = new Table([ + 'columns' => [ + 'label' => ['heading' => 'Label', 'handle' => 'label', 'type' => 'singleline'], + 'enabled' => ['heading' => 'Enabled', 'handle' => 'enabled', 'type' => 'lightswitch'], + ], + 'defaultRowValues' => [[ + 'label' => 'New row', + 'enabled' => true, + ]], + ]); + + $settings = app(FormResolver::class)->resolve($field->settingsForm(), new FormContext); + $defaultRowValues = $settings->nodes[1]->children[1]; + + expect($defaultRowValues->props['label'])->toBe('Default Row Values') + ->and($settings->values['defaultRowValues'])->toBe([[ + 'label' => 'New row', + 'enabled' => true, + ]]) + ->and($defaultRowValues->control->props)->toMatchArray([ + 'minRows' => 1, + 'maxRows' => 1, + ]) + ->and($field->formControl(new FieldContext('details'))->props()['defaultValues'])->toBe([ + 'label' => 'New row', + 'enabled' => true, + ]); +}); + it('keeps column handles scalar while rendering validation errors by cell', function () { $field = new Table([ 'name' => 'Details', From d11a42b5c5d1e5949617a422f0e7e44dbb32033b Mon Sep 17 00:00:00 2001 From: Rias Date: Mon, 21 Sep 2026 17:01:32 +0200 Subject: [PATCH 2/3] Add changelog entry Amp-Thread-ID: https://ampcode.com/threads/T-01a0c462-60db-779c-8896-bc9e3e4076f9 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e56f65d25ea..f80d5bee2d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Removed Craft-managed filesystems and their control panel settings. Volumes and Craft asset transformers now reference Laravel filesystem disks configured in `config/filesystems.php`, and each defines whether its assets have public URLs. Existing filesystem references are migrated to matching disks automatically, with an actionable error if a disk isn’t configured. ([#19650](https://github.com/craftcms/cms/pull/19650)) - Improved performance of element queries, control panel rendering, asset transforms, date formatting, and queue status checks, and fixed related SQLite index and timezone issues. - Added the “Show the Post Date field” and “Show the Expiry Date field” entry type settings. ([#17675](https://github.com/craftcms/cms/pull/17675)) +- Added a “Default Row Values” setting to Table fields. ([#3621](https://github.com/craftcms/cms/issues/3621)) - Added the `autoEagerLoadElements` general config setting (`true` by default), which determines whether element queries should be automatically lazy eager-loaded. ([#19637](https://github.com/craftcms/cms/pull/19637)) - Added Markdown comments to element activity timelines, with support for editing, removing, structured user mentions, and email notifications. - The image editor now supports Undo/Redo. ([#19600](https://github.com/craftcms/cms/pull/19600)) From 8c65ff23f5ca9d653a14bc580b8c7fce85bcc38f Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Tue, 22 Sep 2026 10:35:34 -0700 Subject: [PATCH 3/3] Remove redundant properties/methods --- resources/js/modules/forms/TableControl.vue | 2 -- src/Form/Controls/Table.php | 13 ------------- 2 files changed, 15 deletions(-) diff --git a/resources/js/modules/forms/TableControl.vue b/resources/js/modules/forms/TableControl.vue index ca79bf887e5..21c11f22878 100644 --- a/resources/js/modules/forms/TableControl.vue +++ b/resources/js/modules/forms/TableControl.vue @@ -13,7 +13,6 @@ type TableControlProps = { columns: EditableTableColumns; - defaultValues?: EditableTableRow; allowAdd?: boolean; allowDelete?: boolean; allowReorder?: boolean; @@ -124,7 +123,6 @@ } instance = new EditableTable(id, name, props.control.props.columns, { - defaultValues: props.control.props.defaultValues, allowAdd: props.control.props.allowAdd, allowDelete: props.control.props.allowDelete, allowReorder: props.control.props.allowReorder, diff --git a/src/Form/Controls/Table.php b/src/Form/Controls/Table.php index 0514a437bd4..f61f7e155c4 100644 --- a/src/Form/Controls/Table.php +++ b/src/Form/Controls/Table.php @@ -19,9 +19,6 @@ class Table extends Control /** @var array> */ private array $columns = []; - /** @var array */ - private array $defaultValues = []; - private bool $allowAdd = false; private bool $allowDelete = false; @@ -51,7 +48,6 @@ public static function renderHtml(ControlPayload $control, mixed $value, array $ 'name' => $attributes['name'], 'cols' => $control->props['columns'], 'rows' => $rows, - 'defaultValues' => $control->props['defaultValues'] ?? [], 'allowAdd' => (bool) ($control->props['allowAdd'] ?? false), 'allowDelete' => (bool) ($control->props['allowDelete'] ?? false), 'allowReorder' => (bool) ($control->props['allowReorder'] ?? false), @@ -76,14 +72,6 @@ public function columns(array $columns): static return $this; } - /** @param array $defaultValues */ - public function defaultValues(array $defaultValues): static - { - $this->defaultValues = $defaultValues; - - return $this; - } - public function allowAdd(bool $allowAdd = true): static { $this->allowAdd = $allowAdd; @@ -154,7 +142,6 @@ public function props(mixed $value = null): array { return Arr::whereNotNull([ 'columns' => $this->columns, - 'defaultValues' => $this->defaultValues, 'allowAdd' => $this->allowAdd, 'allowDelete' => $this->allowDelete, 'allowReorder' => $this->allowReorder,