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)
|
* 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ý)
|
* 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)
|
* 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: 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: 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)
|
* 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 $ */
|
/* global $ */
|
||||||
|
|
||||||
export class ListBlockValidationError {
|
export class ListBlockValidationError {
|
||||||
constructor(blockErrors) {
|
constructor(blockErrors, nonBlockErrors) {
|
||||||
this.blockErrors = blockErrors;
|
this.blockErrors = blockErrors;
|
||||||
|
this.nonBlockErrors = nonBlockErrors;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,6 +88,7 @@ export class ListBlock extends BaseSequenceBlock {
|
|||||||
this.blockCounter = 0;
|
this.blockCounter = 0;
|
||||||
this.countInput = dom.find('[data-streamfield-list-count]');
|
this.countInput = dom.find('[data-streamfield-list-count]');
|
||||||
this.sequenceContainer = dom.find('[data-streamfield-list-container]');
|
this.sequenceContainer = dom.find('[data-streamfield-list-container]');
|
||||||
|
this.container = dom;
|
||||||
this.setState(initialState || []);
|
this.setState(initialState || []);
|
||||||
|
|
||||||
if (initialError) {
|
if (initialError) {
|
||||||
@ -130,6 +132,21 @@ export class ListBlock extends BaseSequenceBlock {
|
|||||||
}
|
}
|
||||||
const error = errorList[0];
|
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,
|
// error.blockErrors = a list with the same length as the data,
|
||||||
// with nulls for items without errors
|
// with nulls for items without errors
|
||||||
error.blockErrors.forEach((blockError, blockIndex) => {
|
error.blockErrors.forEach((blockError, blockIndex) => {
|
||||||
|
@ -231,10 +231,25 @@ describe('telepath: wagtail.blocks.ListBlock', () => {
|
|||||||
|
|
||||||
test('setError passes error messages to children', () => {
|
test('setError passes error messages to children', () => {
|
||||||
boundBlock.setError([
|
boundBlock.setError([
|
||||||
new ListBlockValidationError([
|
new ListBlockValidationError(
|
||||||
null,
|
[
|
||||||
[new ValidationError(['Not as good as the first one'])],
|
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();
|
expect(document.body.innerHTML).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
@ -700,3 +700,112 @@ exports[`telepath: wagtail.blocks.ListBlock setError passes error messages to ch
|
|||||||
</button></div>
|
</button></div>
|
||||||
</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 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)
|
* 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ý)
|
* 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
|
Bug fixes
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
@ -17,16 +17,26 @@ __all__ = ['ListBlock', 'ListBlockValidationError']
|
|||||||
|
|
||||||
|
|
||||||
class ListBlockValidationError(ValidationError):
|
class ListBlockValidationError(ValidationError):
|
||||||
def __init__(self, block_errors):
|
def __init__(self, block_errors=None, non_block_errors=None):
|
||||||
self.block_errors = block_errors
|
self.non_block_errors = non_block_errors or ErrorList()
|
||||||
super().__init__('Validation error in ListBlock', params=block_errors)
|
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):
|
class ListBlockValidationErrorAdapter(Adapter):
|
||||||
js_constructor = 'wagtail.blocks.ListBlockValidationError'
|
js_constructor = 'wagtail.blocks.ListBlockValidationError'
|
||||||
|
|
||||||
def js_args(self, error):
|
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
|
@cached_property
|
||||||
def media(self):
|
def media(self):
|
||||||
@ -79,6 +89,7 @@ class ListBlock(Block):
|
|||||||
def clean(self, value):
|
def clean(self, value):
|
||||||
result = []
|
result = []
|
||||||
errors = []
|
errors = []
|
||||||
|
non_block_errors = ErrorList()
|
||||||
for child_val in value:
|
for child_val in value:
|
||||||
try:
|
try:
|
||||||
result.append(self.child_block.clean(child_val))
|
result.append(self.child_block.clean(child_val))
|
||||||
@ -87,8 +98,8 @@ class ListBlock(Block):
|
|||||||
else:
|
else:
|
||||||
errors.append(None)
|
errors.append(None)
|
||||||
|
|
||||||
if any(errors):
|
if any(errors) or non_block_errors:
|
||||||
raise ListBlockValidationError(errors)
|
raise ListBlockValidationError(block_errors=errors, non_block_errors=non_block_errors)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user