0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-12-01 11:41:20 +01:00

Support non-block validation errors on ListBlock (#7322)

This commit is contained in:
Matt Westcott 2021-07-07 23:22:29 +01:00
parent 4eb7c2c019
commit e0ac8ae73d
6 changed files with 165 additions and 11 deletions

View File

@ -15,6 +15,7 @@ Changelog
* Added keyboard and screen reader support to Wagtail user bar (LB Johnston, Storm Heg)
* Add Google Data Studio to the list of oEmbed providers (Petr Dlouhý)
* Added instructions on copying and aliasing pages to the editor's guide in documentation (Vlad Podgurschi)
* Allow ListBlock to raise validation errors that are not attached to an individual child block (Matt Westcott)
* Fix: Invalid filter values for foreign key fields in the API now give an error instead of crashing (Tidjani Dia)
* Fix: Ordering specified in `construct_explorer_page_queryset` hook is now taken into account again by the page explorer API (Andre Fonseca)
* Fix: Deleting a page from its listing view no longer results in a 404 error (Tidjani Dia)

View File

@ -6,8 +6,9 @@ import { escapeHtml as h } from '../../../utils/text';
/* global $ */
export class ListBlockValidationError {
constructor(blockErrors) {
constructor(blockErrors, nonBlockErrors) {
this.blockErrors = blockErrors;
this.nonBlockErrors = nonBlockErrors;
}
}
@ -87,6 +88,7 @@ export class ListBlock extends BaseSequenceBlock {
this.blockCounter = 0;
this.countInput = dom.find('[data-streamfield-list-count]');
this.sequenceContainer = dom.find('[data-streamfield-list-container]');
this.container = dom;
this.setState(initialState || []);
if (initialError) {
@ -130,6 +132,21 @@ export class ListBlock extends BaseSequenceBlock {
}
const error = errorList[0];
// Non block errors
const container = this.container[0];
container.querySelectorAll(':scope > .help-block.help-critical').forEach(element => element.remove());
if (error.nonBlockErrors.length > 0) {
// Add a help block for each error raised
error.nonBlockErrors.forEach(nonBlockError => {
const errorElement = document.createElement('p');
errorElement.classList.add('help-block');
errorElement.classList.add('help-critical');
errorElement.innerHTML = h(nonBlockError.messages[0]);
container.insertBefore(errorElement, container.childNodes[0]);
});
}
// error.blockErrors = a list with the same length as the data,
// with nulls for items without errors
error.blockErrors.forEach((blockError, blockIndex) => {

View File

@ -231,10 +231,25 @@ describe('telepath: wagtail.blocks.ListBlock', () => {
test('setError passes error messages to children', () => {
boundBlock.setError([
new ListBlockValidationError([
null,
[new ValidationError(['Not as good as the first one'])],
]),
new ListBlockValidationError(
[
null,
[new ValidationError(['Not as good as the first one'])],
],
[]
),
]);
expect(document.body.innerHTML).toMatchSnapshot();
});
test('setError renders non-block errors', () => {
boundBlock.setError([
new ListBlockValidationError(
[null, null],
[
new ValidationError(['At least three blocks are required']),
]
),
]);
expect(document.body.innerHTML).toMatchSnapshot();
});

View File

@ -700,3 +700,112 @@ exports[`telepath: wagtail.blocks.ListBlock setError passes error messages to ch
</button></div>
</div>"
`;
exports[`telepath: wagtail.blocks.ListBlock setError renders non-block errors 1`] = `
"<span>
<div class=\\"help\\">
<div class=\\"icon-help\\">?</div>
use <strong>a few</strong> of these
</div>
</span><div class=\\"c-sf-container \\"><p class=\\"help-block help-critical\\">At least three blocks are required</p>
<input type=\\"hidden\\" name=\\"the-prefix-count\\" data-streamfield-list-count=\\"\\" value=\\"2\\">
<div data-streamfield-list-container=\\"\\"><button type=\\"button\\" title=\\"Add\\" data-streamfield-list-add=\\"\\" class=\\"c-sf-add-button c-sf-add-button--visible\\">
<i aria-hidden=\\"true\\">+</i>
</button><div aria-hidden=\\"false\\" data-contentpath-disabled=\\"\\">
<input type=\\"hidden\\" name=\\"the-prefix-0-deleted\\" value=\\"\\">
<input type=\\"hidden\\" name=\\"the-prefix-0-order\\" value=\\"0\\">
<input type=\\"hidden\\" name=\\"the-prefix-0-type\\" value=\\"\\">
<input type=\\"hidden\\" name=\\"the-prefix-0-id\\" value=\\"\\">
<div>
<div class=\\"c-sf-container__block-container\\">
<div class=\\"c-sf-block\\">
<div data-block-header=\\"\\" class=\\"c-sf-block__header c-sf-block__header--collapsible\\">
<span class=\\"c-sf-block__header__icon\\">
<i class=\\"icon icon-pilcrow\\"></i>
</span>
<h3 data-block-title=\\"\\" class=\\"c-sf-block__header__title\\"></h3>
<div class=\\"c-sf-block__actions\\">
<span class=\\"c-sf-block__type\\"></span>
<button type=\\"button\\" data-move-up-button=\\"\\" class=\\"c-sf-block__actions__single\\" disabled=\\"\\" title=\\"Move up\\">
<i class=\\"icon icon-arrow-up\\" aria-hidden=\\"true\\"></i>
</button>
<button type=\\"button\\" data-move-down-button=\\"\\" class=\\"c-sf-block__actions__single\\" title=\\"Move down\\">
<i class=\\"icon icon-arrow-down\\" aria-hidden=\\"true\\"></i>
</button>
<button type=\\"button\\" data-duplicate-button=\\"\\" class=\\"c-sf-block__actions__single\\" title=\\"Duplicate\\">
<i class=\\"icon icon-duplicate\\" aria-hidden=\\"true\\"></i>
</button>
<button type=\\"button\\" data-delete-button=\\"\\" class=\\"c-sf-block__actions__single\\" title=\\"Delete\\">
<i class=\\"icon icon-bin\\" aria-hidden=\\"true\\"></i>
</button>
</div>
</div>
<div data-block-content=\\"\\" class=\\"c-sf-block__content\\" aria-hidden=\\"false\\">
<div class=\\"c-sf-block__content-inner\\">
<div class=\\"field char_field widget-admin_auto_height_text_input fieldname-\\">
<div class=\\"field-content\\">
<div class=\\"input\\">
<p name=\\"the-prefix-0-value\\" id=\\"the-prefix-0-value\\">The widget</p>
<span></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div><button type=\\"button\\" title=\\"Add\\" data-streamfield-list-add=\\"\\" class=\\"c-sf-add-button c-sf-add-button--visible\\">
<i aria-hidden=\\"true\\">+</i>
</button><div aria-hidden=\\"false\\" data-contentpath-disabled=\\"\\">
<input type=\\"hidden\\" name=\\"the-prefix-1-deleted\\" value=\\"\\">
<input type=\\"hidden\\" name=\\"the-prefix-1-order\\" value=\\"1\\">
<input type=\\"hidden\\" name=\\"the-prefix-1-type\\" value=\\"\\">
<input type=\\"hidden\\" name=\\"the-prefix-1-id\\" value=\\"\\">
<div>
<div class=\\"c-sf-container__block-container\\">
<div class=\\"c-sf-block\\">
<div data-block-header=\\"\\" class=\\"c-sf-block__header c-sf-block__header--collapsible\\">
<span class=\\"c-sf-block__header__icon\\">
<i class=\\"icon icon-pilcrow\\"></i>
</span>
<h3 data-block-title=\\"\\" class=\\"c-sf-block__header__title\\"></h3>
<div class=\\"c-sf-block__actions\\">
<span class=\\"c-sf-block__type\\"></span>
<button type=\\"button\\" data-move-up-button=\\"\\" class=\\"c-sf-block__actions__single\\" title=\\"Move up\\">
<i class=\\"icon icon-arrow-up\\" aria-hidden=\\"true\\"></i>
</button>
<button type=\\"button\\" data-move-down-button=\\"\\" class=\\"c-sf-block__actions__single\\" disabled=\\"\\" title=\\"Move down\\">
<i class=\\"icon icon-arrow-down\\" aria-hidden=\\"true\\"></i>
</button>
<button type=\\"button\\" data-duplicate-button=\\"\\" class=\\"c-sf-block__actions__single\\" title=\\"Duplicate\\">
<i class=\\"icon icon-duplicate\\" aria-hidden=\\"true\\"></i>
</button>
<button type=\\"button\\" data-delete-button=\\"\\" class=\\"c-sf-block__actions__single\\" title=\\"Delete\\">
<i class=\\"icon icon-bin\\" aria-hidden=\\"true\\"></i>
</button>
</div>
</div>
<div data-block-content=\\"\\" class=\\"c-sf-block__content\\" aria-hidden=\\"false\\">
<div class=\\"c-sf-block__content-inner\\">
<div class=\\"field char_field widget-admin_auto_height_text_input fieldname-\\">
<div class=\\"field-content\\">
<div class=\\"input\\">
<p name=\\"the-prefix-1-value\\" id=\\"the-prefix-1-value\\">The widget</p>
<span></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div><button type=\\"button\\" title=\\"Add\\" data-streamfield-list-add=\\"\\" class=\\"c-sf-add-button c-sf-add-button--visible\\">
<i aria-hidden=\\"true\\">+</i>
</button></div>
</div>"
`;

View File

@ -23,6 +23,7 @@ Other features
* Added keyboard and screen reader support to Wagtail user bar (LB Johnston, Storm Heg)
* Added instructions on copying and aliasing pages to the editor's guide in documentation (Vlad Podgurschi)
* Add Google Data Studio to the list of oEmbed providers (Petr Dlouhý)
* Allow ListBlock to raise validation errors that are not attached to an individual child block (Matt Westcott)
Bug fixes
~~~~~~~~~

View File

@ -17,16 +17,26 @@ __all__ = ['ListBlock', 'ListBlockValidationError']
class ListBlockValidationError(ValidationError):
def __init__(self, block_errors):
self.block_errors = block_errors
super().__init__('Validation error in ListBlock', params=block_errors)
def __init__(self, block_errors=None, non_block_errors=None):
self.non_block_errors = non_block_errors or ErrorList()
self.block_errors = block_errors or []
params = {}
if block_errors:
params['block_errors'] = block_errors
if non_block_errors:
params['non_block_errors'] = non_block_errors
super().__init__('Validation error in ListBlock', params=params)
class ListBlockValidationErrorAdapter(Adapter):
js_constructor = 'wagtail.blocks.ListBlockValidationError'
def js_args(self, error):
return [[elist.as_data() if elist is not None else elist for elist in error.block_errors]]
return [
[elist.as_data() if elist is not None else elist for elist in error.block_errors],
error.non_block_errors.as_data(),
]
@cached_property
def media(self):
@ -79,6 +89,7 @@ class ListBlock(Block):
def clean(self, value):
result = []
errors = []
non_block_errors = ErrorList()
for child_val in value:
try:
result.append(self.child_block.clean(child_val))
@ -87,8 +98,8 @@ class ListBlock(Block):
else:
errors.append(None)
if any(errors):
raise ListBlockValidationError(errors)
if any(errors) or non_block_errors:
raise ListBlockValidationError(block_errors=errors, non_block_errors=non_block_errors)
return result