Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table field improvements #13231

Merged
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
54 changes: 48 additions & 6 deletions src/fields/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ public static function valueType(): string
return 'array|null';
}

/**
* @var bool Whether the rows should be static.
* @since 5.0.0
*/
public bool $staticRows = false;

/**
* @var string|null Custom add row button label
*/
Expand Down Expand Up @@ -156,6 +162,11 @@ public function init(): void
if (!isset($this->addRowLabel)) {
$this->addRowLabel = Craft::t('app', 'Add a row');
}

if ($this->staticRows) {
$this->minRows = null;
$this->maxRows = null;
}
}

/**
Expand Down Expand Up @@ -236,6 +247,7 @@ public function getSettingsHtml(): ?string
'date' => Craft::t('app', 'Date'),
'select' => Craft::t('app', 'Dropdown'),
'email' => Craft::t('app', 'Email'),
'heading' => Craft::t('app', 'Row heading'),
'lightswitch' => Craft::t('app', 'Lightswitch'),
'multiline' => Craft::t('app', 'Multi-line text'),
'number' => Craft::t('app', 'Number'),
Expand Down Expand Up @@ -305,14 +317,23 @@ public function getSettingsHtml(): ?string
'initJs' => false,
]);

// Replace heading columns with singleline, for the Default Values table
$columns = array_map(function(array $column) {
if ($column['type'] === 'heading') {
$column['type'] = 'singleline';
$column['class'] = 'heading';
}
return $column;
}, $this->columns);

$view = Craft::$app->getView();

$view->registerAssetBundle(TimepickerAsset::class);
$view->registerAssetBundle(TableSettingsAsset::class);
$view->registerJs('new Craft.TableFieldSettings(' .
Json::encode($view->namespaceInputName('columns')) . ', ' .
Json::encode($view->namespaceInputName('defaults')) . ', ' .
Json::encode($this->columns) . ', ' .
Json::encode($columns) . ', ' .
Json::encode($this->defaults ?? []) . ', ' .
Json::encode($columnSettings) . ', ' .
Json::encode($dropdownSettingsHtml) . ', ' .
Expand All @@ -333,7 +354,7 @@ public function getSettingsHtml(): ?string
'allowAdd' => true,
'allowReorder' => true,
'allowDelete' => true,
'cols' => $this->columns,
'cols' => $columns,
'rows' => $this->defaults,
'initJs' => false,
]);
Expand Down Expand Up @@ -413,20 +434,36 @@ public function normalizeValueFromRequest(mixed $value, ?ElementInterface $eleme

private function _normalizeValueInternal(mixed $value, ?ElementInterface $element, bool $fromRequest): ?array
{
$defaults = $this->defaults ?? [];

if (is_string($value) && !empty($value)) {
$value = Json::decodeIfJson($value);
} elseif ($value === null && $this->isFresh($element)) {
$value = array_values($this->defaults ?? []);
$value = $defaults;
}

if (!is_array($value) || empty($this->columns)) {
return null;
}

// Normalize the values and make them accessible from both the col IDs and the handles
foreach ($value as &$row) {
$value = array_values($value);

if ($this->staticRows) {
$valueRows = count($value);
$totalRows = count($defaults);
if ($valueRows < $totalRows) {
$value = array_pad($value, $totalRows, []);
} elseif ($valueRows > $totalRows) {
array_splice($value, $totalRows);
}
}

foreach ($value as $rowIndex => &$row) {
foreach ($this->columns as $colId => $col) {
if (array_key_exists($colId, $row)) {
if ($col['type'] === 'heading') {
$cellValue = $defaults[$rowIndex][$colId] ?? '';
} elseif (array_key_exists($colId, $row)) {
$cellValue = $row[$colId];
} elseif ($col['handle'] && array_key_exists($col['handle'], $row)) {
$cellValue = $row[$col['handle']];
Expand Down Expand Up @@ -458,7 +495,11 @@ public function serializeValue(mixed $value, ?ElementInterface $element = null):

foreach ($value as $row) {
$serializedRow = [];
foreach (array_keys($this->columns) as $colId) {
foreach ($this->columns as $colId => $column) {
if ($column['type'] === 'heading') {
continue;
}

$value = $row[$colId];

if (is_string($value) && !$supportsMb4) {
Expand Down Expand Up @@ -678,6 +719,7 @@ private function _getInputHtml(mixed $value, ?ElementInterface $element, bool $s
'minRows' => $this->minRows,
'maxRows' => $this->maxRows,
'static' => $static,
'staticRows' => $this->staticRows,
'allowAdd' => true,
'allowDelete' => true,
'allowReorder' => true,
Expand Down
62 changes: 36 additions & 26 deletions src/templates/_components/fieldtypes/Table/settings.twig
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,45 @@
{{ columnsField|raw }}
{{ defaultsField|raw }}

{{ forms.textField({
label: "Min Rows"|t('app'),
instructions: "The minimum number of rows the field is allowed to have."|t('app'),
id: 'minRows',
name: 'minRows',
value: field.minRows,
size: 3,
errors: field.getErrors('minRows')
{{ forms.lightswitchField({
label: 'Static Rows'|t('app'),
instructions: 'Whether the table rows should be restricted to those defined by the “Default Values” setting.'|t('app'),
name: 'staticRows',
on: field.staticRows,
reverseToggle: 'dynamic-row-settings',
}) }}

{{ forms.textField({
label: "Max Rows"|t('app'),
instructions: "The maximum number of rows the field is allowed to have."|t('app'),
id: 'maxRows',
name: 'maxRows',
value: field.maxRows,
size: 3,
errors: field.getErrors('maxRows')
}) }}
<fieldset id="dynamic-row-settings"{% if field.staticRows %} class="hidden indent"{% endif %}>
{{ forms.textField({
label: "Min Rows"|t('app'),
instructions: "The minimum number of rows the field is allowed to have."|t('app'),
id: 'minRows',
name: 'minRows',
value: field.minRows,
size: 3,
errors: field.getErrors('minRows')
}) }}

{{ forms.textField({
label: "Add Row Label"|t('app'),
instructions: "Insert the button label for adding a new row to the table."|t('app'),
id: 'addRowLabel',
name: 'addRowLabel',
value: field.addRowLabel,
size: 20,
errors: field.getErrors('addRowLabel')
}) }}
{{ forms.textField({
label: "Max Rows"|t('app'),
instructions: "The maximum number of rows the field is allowed to have."|t('app'),
id: 'maxRows',
name: 'maxRows',
value: field.maxRows,
size: 3,
errors: field.getErrors('maxRows')
}) }}

{{ forms.textField({
label: "Add Row Label"|t('app'),
instructions: "Insert the button label for adding a new row to the table."|t('app'),
id: 'addRowLabel',
name: 'addRowLabel',
value: field.addRowLabel,
size: 20,
errors: field.getErrors('addRowLabel')
}) }}
</fieldset>

{% if craft.app.db.isMysql %}
<hr>
Expand Down
61 changes: 33 additions & 28 deletions src/templates/_includes/forms/editableTable.twig
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@
{%- set tableAttributes = tableAttributes|merge(('<div ' ~ block('attr') ~ '>')|parseAttr, recursive=true) %}
{% endif %}

{% for col in cols %}
{%- switch col.type %}
{%- case 'time' %}
{%- do view.registerAssetBundle('craft\\web\\assets\\timepicker\\TimepickerAsset') %}
{%- case 'template' %}
{%- do view.registerAssetBundle("craft\\web\\assets\\vue\\VueAsset") %}
{%- endswitch %}
{% endfor %}

<span aria-live="assertive" class="visually-hidden" data-status-message></span>
{% tag 'table' with tableAttributes %}
{% for col in cols %}
Expand All @@ -62,34 +71,30 @@
{% if allowDelete %}<col>{% endif %}
{% if allowReorder %}<col>{% endif %}
{% endif %}
<thead>
<tr>
{% for col in cols %}
{%- switch col.type %}
{%- case 'time' %}
{%- do view.registerAssetBundle('craft\\web\\assets\\timepicker\\TimepickerAsset') %}
{%- case 'template' %}
{%- do view.registerAssetBundle("craft\\web\\assets\\vue\\VueAsset") %}
{%- endswitch %}
{% set columnHeadingId = "#{id}-heading-#{loop.index}" %}
<th id="{{ columnHeadingId }}" scope="col" class="{{ _self.cellClass(fullWidth, col, col.class ?? []) }}">
{%- if col.headingHtml is defined %}
{{- col.headingHtml|raw }}
{%- elseif col.heading ?? false %}
{{- col.heading }}
{%- else %}
&nbsp;
{%- endif %}
{%- if col.info is defined -%}
<span class="info">{{ col.info|md|raw }}</span>
{%- endif -%}
</th>
{% endfor %}
{% if (allowDelete or allowReorder) %}
<th colspan="{{ not allowDelete or not allowReorder ? 1 : 2 }}" scope="colgroup"><span class="visually-hidden">{{ 'Row actions'|t('app') }}</span></th>
{% endif %}
</tr>
</thead>
{% if cols|filter(c => (c.headingHtml ?? c.heading ?? c.info ?? '') is not same as(''))|length %}
<thead>
<tr>
{% for col in cols %}
{% set columnHeadingId = "#{id}-heading-#{loop.index}" %}
<th id="{{ columnHeadingId }}" scope="col" class="{{ _self.cellClass(fullWidth, col, col.class ?? []) }}">
{%- if col.headingHtml is defined %}
{{- col.headingHtml|raw }}
{%- elseif col.heading ?? false %}
{{- col.heading }}
{%- else %}
&nbsp;
{%- endif %}
{%- if col.info is defined -%}
<span class="info">{{ col.info|md|raw }}</span>
{%- endif -%}
</th>
{% endfor %}
{% if (allowDelete or allowReorder) %}
<th colspan="{{ not allowDelete or not allowReorder ? 1 : 2 }}" scope="colgroup"><span class="visually-hidden">{{ 'Row actions'|t('app') }}</span></th>
{% endif %}
</tr>
</thead>
{% endif %}
<tbody>
{% for rowId, row in rows %}
{% set rowNumber = loop.index %}
Expand Down
3 changes: 3 additions & 0 deletions src/translations/en/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,7 @@
'Row actions' => 'Row actions',
'Row could not be added. Maximum number of rows reached.' => 'Row could not be added. Maximum number of rows reached.',
'Row could not be deleted. Minimum number of rows reached.' => 'Row could not be deleted. Minimum number of rows reached.',
'Row heading' => 'Row heading',
'Rule Type' => 'Rule Type',
'SVG file recommended. The logo will be displayed at {size} wide.' => 'SVG file recommended. The logo will be displayed at {size} wide.',
'Same as asset filesystem' => 'Same as asset filesystem',
Expand Down Expand Up @@ -1369,6 +1370,7 @@
'Square SVG file recommended. The logo will be displayed at {size} by {size}.' => 'Square SVG file recommended. The logo will be displayed at {size} by {size}.',
'Stack Trace' => 'Stack Trace',
'State' => 'State',
'Static Rows' => 'Static Rows',
'Status updated, with some failures due to validation errors.' => 'Status updated, with some failures due to validation errors.',
'Status updated.' => 'Status updated.',
'Status' => 'Status',
Expand Down Expand Up @@ -1762,6 +1764,7 @@
'Whether remotely-stored images should be downloaded and stored locally, to speed up transform generation.' => 'Whether remotely-stored images should be downloaded and stored locally, to speed up transform generation.',
'Whether the site menu should be shown for {type} selection modals.' => 'Whether the site menu should be shown for {type} selection modals.',
'Whether the structure of the related {type} should be maintained.' => 'Whether the structure of the related {type} should be maintained.',
'Whether the table rows should be restricted to those defined by the “Default Values” setting.' => 'Whether the table rows should be restricted to those defined by the “Default Values” setting.',
'Whether this can be dismissed by a user and not shown again.' => 'Whether this can be dismissed by a user and not shown again.',
'Whether to show files that the user doesn’t have permission to view, per the “View files uploaded by other users” permission.' => 'Whether to show files that the user doesn’t have permission to view, per the “View files uploaded by other users” permission.',
'Whether to show volumes that the user doesn’t have permission to view.' => 'Whether to show volumes that the user doesn’t have permission to view.',
Expand Down
2 changes: 1 addition & 1 deletion src/web/assets/cp/dist/cp.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/web/assets/cp/dist/cp.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/web/assets/cp/dist/css/cp.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/web/assets/cp/dist/css/cp.css.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/web/assets/cp/src/css/_main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3400,6 +3400,7 @@ table.editable {
border: 1px solid var(--gray-200);

th,
td.heading,
td.action {
color: var(--medium-text-color);
font-weight: normal;
Expand Down
3 changes: 2 additions & 1 deletion src/web/assets/cp/src/js/EditableTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,8 @@ Craft.EditableTable.Row = Garnish.Base.extend(
if (
col.autopopulate &&
typeof textareasByColId[col.autopopulate] !== 'undefined' &&
!textareasByColId[colId].val()
!textareasByColId[colId].val() &&
!textareasByColId[col.autopopulate].val()
) {
new Craft.HandleGenerator(
textareasByColId[colId],
Expand Down
Loading