diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index b4286d6323..ea5bf83971 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -38,6 +38,7 @@ Changelog
* Move locale selector in generic IndexView to a filter (Sage Abdullah)
* Add ability to customise a page's copy form (Neeraj Yetheendran)
* Add optional caption field to `TypedTableBlock` (Tommaso Amici, Cynthia Kiser)
+ * Switch the `TableBlock` header controls to a field that requires user input (Bhuvnesh Sharma, Aman Pandey, Cynthia Kiser)
* Fix: Update system check for overwriting storage backends to recognise the `STORAGES` setting introduced in Django 4.2 (phijma-leukeleu)
* Fix: Prevent password change form from raising a validation error when browser autocomplete fills in the "Old password" field (Chiemezuo Akujobi)
* Fix: Ensure that the legacy dropdown options, when closed, do not get accidentally clicked by other interactions wide viewports (CheesyPhoenix, Christer Jensen)
diff --git a/client/src/entrypoints/contrib/table_block/__snapshots__/table.test.js.snap b/client/src/entrypoints/contrib/table_block/__snapshots__/table.test.js.snap
index fe6ebd08db..067b667b44 100644
--- a/client/src/entrypoints/contrib/table_block/__snapshots__/table.test.js.snap
+++ b/client/src/entrypoints/contrib/table_block/__snapshots__/table.test.js.snap
@@ -3,26 +3,23 @@
exports[`telepath: wagtail.widgets.TableInput it renders correctly 1`] = `
"
Affichez la première colonne sous forme d'en-tête.
-
-
-
-
-
+
+
+
Quelles cellules doivent être affichées en tant qu'en-têtes?
diff --git a/client/src/entrypoints/contrib/table_block/table.js b/client/src/entrypoints/contrib/table_block/table.js
index 134b7edd40..a0292070d3 100644
--- a/client/src/entrypoints/contrib/table_block/table.js
+++ b/client/src/entrypoints/contrib/table_block/table.js
@@ -7,12 +7,14 @@ import { hasOwn } from '../../../utils/hasOwn';
function initTable(id, tableOptions) {
const containerId = id + '-handsontable-container';
- const tableHeaderCheckboxId = id + '-handsontable-header';
- const colHeaderCheckboxId = id + '-handsontable-col-header';
+ var tableHeaderId = id + '-handsontable-header';
+ var colHeaderId = id + '-handsontable-col-header';
+ var headerChoiceId = id + '-table-header-choice';
const tableCaptionId = id + '-handsontable-col-caption';
const hiddenStreamInput = $('#' + id);
- const tableHeaderCheckbox = $('#' + tableHeaderCheckboxId);
- const colHeaderCheckbox = $('#' + colHeaderCheckboxId);
+ var tableHeader = $('#' + tableHeaderId);
+ var colHeader = $('#' + colHeaderId);
+ var headerChoice = $('#' + headerChoiceId);
const tableCaption = $('#' + tableCaptionId);
const finalOptions = {};
let hot = null;
@@ -52,18 +54,12 @@ function initTable(id, tableOptions) {
}
if (dataForForm !== null) {
- if (hasOwn(dataForForm, 'first_row_is_table_header')) {
- tableHeaderCheckbox.prop(
- 'checked',
- dataForForm.first_row_is_table_header,
- );
- }
- if (hasOwn(dataForForm, 'first_col_is_header')) {
- colHeaderCheckbox.prop('checked', dataForForm.first_col_is_header);
- }
if (hasOwn(dataForForm, 'table_caption')) {
tableCaption.prop('value', dataForForm.table_caption);
}
+ if (hasOwn(dataForForm, 'table_header_choice')) {
+ headerChoice.prop('value', dataForForm.table_header_choice);
+ }
}
if (!hasOwn(tableOptions, 'width') || !hasOwn(tableOptions, 'height')) {
@@ -123,8 +119,9 @@ function initTable(id, tableOptions) {
data: hot.getData(),
cell: cell,
mergeCells: mergeCells,
- first_row_is_table_header: tableHeaderCheckbox.prop('checked'),
- first_col_is_header: colHeaderCheckbox.prop('checked'),
+ first_row_is_table_header: tableHeader.val(),
+ first_col_is_header: colHeader.val(),
+ table_header_choice: headerChoice.val(),
table_caption: tableCaption.val(),
}),
);
@@ -169,11 +166,7 @@ function initTable(id, tableOptions) {
persist();
};
- tableHeaderCheckbox.on('change', () => {
- persist();
- });
-
- colHeaderCheckbox.on('change', () => {
+ headerChoice.on('change', () => {
persist();
});
@@ -238,26 +231,23 @@ class TableInput {
const container = document.createElement('div');
container.innerHTML = `
-
-
-
-
${this.strings['Display the first row as a header.']}
-
-
-
-
-
-
-
-
-
-
-
${this.strings['Display the first column as a header.']}
-
-
-
-
-
+
+
+
${this.strings['Which cells should be displayed as headers?']}
diff --git a/client/src/entrypoints/contrib/table_block/table.test.js b/client/src/entrypoints/contrib/table_block/table.test.js
index 26feabba33..462f7f35f2 100644
--- a/client/src/entrypoints/contrib/table_block/table.test.js
+++ b/client/src/entrypoints/contrib/table_block/table.test.js
@@ -33,11 +33,15 @@ const TEST_OPTIONS = {
};
const TEST_STRINGS = {
- 'Row header': 'Row header',
- 'Display the first row as a header.': 'Display the first row as a header.',
- 'Column header': 'Column header',
- 'Display the first column as a header.':
- 'Display the first column as a header.',
+ 'Table headers': 'Table headers',
+ 'Display the first row as a header': 'Display the first row as a header',
+ 'Display the first column as a header':
+ 'Display the first column as a header',
+ 'Display the first row AND first column as headers':
+ 'Display the first row AND first column as headers',
+ 'No headers': 'No headers',
+ 'Which cells should be displayed as headers?':
+ 'Which cells should be displayed as headers?',
'Table caption': 'Table caption',
'A heading that identifies the overall topic of the table, and is useful for screen reader users.':
@@ -155,12 +159,16 @@ describe('telepath: wagtail.widgets.TableInput', () => {
test('translation', () => {
testStrings = {
- 'Row header': 'En-tête de ligne',
- 'Display the first row as a header.':
- "Affichez la première ligne sous forme d'en-tête.",
- 'Column header': 'En-tête de colonne',
- 'Display the first column as a header.':
- "Affichez la première colonne sous forme d'en-tête.",
+ 'Table headers': 'En-têtes de tableau',
+ 'Display the first row as a header':
+ "Afficher la première ligne sous forme d'en-tête",
+ 'Display the first column as a header':
+ "Afficher la première colonne sous forme d'en-tête",
+ 'Display the first row AND first column as headers':
+ "Afficher la première ligne ET la première colonne sous forme d'en-têtes",
+ 'No headers': "Pas d'en-têtes",
+ 'Which cells should be displayed as headers?':
+ "Quelles cellules doivent être affichées en tant qu'en-têtes?",
'Table caption': 'Légende du tableau',
'A heading that identifies the overall topic of the table, and is useful for screen reader users.':
diff --git a/docs/releases/6.0.md b/docs/releases/6.0.md
index 2016741296..4b004d7b38 100644
--- a/docs/releases/6.0.md
+++ b/docs/releases/6.0.md
@@ -60,6 +60,7 @@ Thank you to Thibaud Colas and Badr Fourane for their work on this feature.
* Show character counts on RichTextBlock with `max_length` (Elhussein Almasri)
* Move locale selector in generic IndexView to a filter (Sage Abdullah)
* Add optional caption field to `TypedTableBlock` (Tommaso Amici, Cynthia Kiser)
+ * Switch the `TableBlock` header controls to a field that requires user input (Bhuvnesh Sharma, Aman Pandey, Cynthia Kiser)
### Bug fixes
diff --git a/wagtail/contrib/table_block/blocks.py b/wagtail/contrib/table_block/blocks.py
index dcacd9b4f7..6e5e420013 100644
--- a/wagtail/contrib/table_block/blocks.py
+++ b/wagtail/contrib/table_block/blocks.py
@@ -1,6 +1,9 @@
import json
from django import forms
+from django.core.exceptions import ValidationError
+from django.forms.fields import Field
+from django.forms.utils import ErrorList
from django.template.loader import render_to_string
from django.utils import translation
from django.utils.functional import cached_property
@@ -68,12 +71,18 @@ class TableInputAdapter(WidgetAdapter):
def js_args(self, widget):
strings = {
"Row header": _("Row header"),
- "Display the first row as a header.": _(
- "Display the first row as a header."
+ "Table headers": _("Table headers"),
+ "Display the first row as a header": _("Display the first row as a header"),
+ "Display the first column as a header": _(
+ "Display the first column as a header"
),
"Column header": _("Column header"),
- "Display the first column as a header.": _(
- "Display the first column as a header."
+ "Display the first row AND first column as headers": _(
+ "Display the first row AND first column as headers"
+ ),
+ "No headers": _("No headers"),
+ "Which cells should be displayed as headers?": _(
+ "Which cells should be displayed as headers?"
),
"Table caption": _("Table caption"),
"A heading that identifies the overall topic of the table, and is useful for screen reader users.": _(
@@ -117,6 +126,44 @@ class TableBlock(FieldBlock):
def value_for_form(self, value):
return json.dumps(value)
+ def to_python(self, value):
+ """
+ If value came from a table block stored before Wagtail 6.0, we need to set an appropriate
+ value for the header choice. I would really like to have this default to "" and force the
+ editor to reaffirm they don't want any headers, but that woud be a breaking change.
+ """
+ if not value.get("table_header_choice", ""):
+ if value.get("first_row_is_table_header", False) and value.get(
+ "first_col_is_header", False
+ ):
+ value["table_header_choice"] = "both"
+ elif value.get("first_row_is_table_header", False):
+ value["table_header_choice"] = "row"
+ elif value.get("first_col_is_header", False):
+ value["table_header_choice"] = "col"
+ else:
+ value["table_header_choice"] = "neither"
+ return value
+
+ def clean(self, value):
+ if not value:
+ return value
+
+ if value.get("table_header_choice", ""):
+ value["first_row_is_table_header"] = value["table_header_choice"] in [
+ "row",
+ "both",
+ ]
+ value["first_col_is_header"] = value["table_header_choice"] in [
+ "column",
+ "both",
+ ]
+ else:
+ # Ensure we have a choice for the table_header_choice
+ errors = ErrorList(Field.default_error_messages["required"])
+ raise ValidationError("Validation error in TableBlock", params=errors)
+ return self.value_from_form(self.field.clean(self.value_for_form(value)))
+
def get_form_state(self, value):
# pass state to frontend as a JSON-ish dict - do not serialise to a JSON string
return value
diff --git a/wagtail/contrib/table_block/tests.py b/wagtail/contrib/table_block/tests.py
index 085dbe8f41..51e9eff6dd 100644
--- a/wagtail/contrib/table_block/tests.py
+++ b/wagtail/contrib/table_block/tests.py
@@ -116,7 +116,7 @@ class TestTableBlock(TestCase):
"""
self.assertHTMLEqual(result, expected)
- def test_do_not_render_html(self):
+ def test_do_not_render_html_by_default(self):
"""
Ensure that raw html doesn't render
by default.
@@ -145,6 +145,36 @@ class TestTableBlock(TestCase):
result = block.render(value)
self.assertHTMLEqual(result, expected)
+ def test_does_render_html_if_allowed(self):
+ """
+ Ensure html renders if table_options set renderer to allow html
+ """
+ value = {
+ "first_row_is_table_header": False,
+ "first_col_is_header": False,
+ "data": [
+ ["
+ """
+ block = TableBlock()
+ result = block.render(value)
+ self.assertHTMLEqual(result, expected)
+
def test_value_for_and_from_form(self):
"""
Make sure we get back good json and make