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:
parent
4eb7c2c019
commit
e0ac8ae73d
@ -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)
|
||||
|
@ -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) => {
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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>"
|
||||
`;
|
||||
|
@ -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
|
||||
~~~~~~~~~
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user