fix: Error UI improvements for nested lists/objects (#3726)
This commit is contained in:
parent
2ecafd3354
commit
397857855b
@ -15,9 +15,6 @@ import {
|
|||||||
assertEntryDeleted,
|
assertEntryDeleted,
|
||||||
assertWorkflowStatus,
|
assertWorkflowStatus,
|
||||||
updateWorkflowStatusInEditor,
|
updateWorkflowStatusInEditor,
|
||||||
validateObjectFieldsAndExit,
|
|
||||||
validateNestedObjectFieldsAndExit,
|
|
||||||
validateListFieldsAndExit,
|
|
||||||
unpublishEntry,
|
unpublishEntry,
|
||||||
publishEntryInEditor,
|
publishEntryInEditor,
|
||||||
duplicateEntry,
|
duplicateEntry,
|
||||||
@ -26,7 +23,7 @@ import {
|
|||||||
publishAndCreateNewEntryInEditor,
|
publishAndCreateNewEntryInEditor,
|
||||||
publishAndDuplicateEntryInEditor,
|
publishAndDuplicateEntryInEditor,
|
||||||
} from '../utils/steps';
|
} from '../utils/steps';
|
||||||
import { setting1, setting2, workflowStatus, editorStatus, publishTypes } from '../utils/constants';
|
import { workflowStatus, editorStatus, publishTypes } from '../utils/constants';
|
||||||
|
|
||||||
const entry1 = {
|
const entry1 = {
|
||||||
title: 'first title',
|
title: 'first title',
|
||||||
@ -74,21 +71,6 @@ describe('Test Backend Editorial Workflow', () => {
|
|||||||
exitEditor();
|
exitEditor();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can validate object fields', () => {
|
|
||||||
login();
|
|
||||||
validateObjectFieldsAndExit(setting1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can validate fields nested in an object field', () => {
|
|
||||||
login();
|
|
||||||
validateNestedObjectFieldsAndExit(setting1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can validate list fields', () => {
|
|
||||||
login();
|
|
||||||
validateListFieldsAndExit(setting2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can publish an editorial workflow entry', () => {
|
it('can publish an editorial workflow entry', () => {
|
||||||
login();
|
login();
|
||||||
createPostAndExit(entry1);
|
createPostAndExit(entry1);
|
||||||
|
109
cypress/integration/field_validations_spec.js
Normal file
109
cypress/integration/field_validations_spec.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import '../utils/dismiss-local-backup';
|
||||||
|
import {
|
||||||
|
login,
|
||||||
|
validateObjectFieldsAndExit,
|
||||||
|
validateNestedObjectFieldsAndExit,
|
||||||
|
validateListFieldsAndExit,
|
||||||
|
validateNestedListFieldsAndExit,
|
||||||
|
} from '../utils/steps';
|
||||||
|
import { setting1, setting2 } from '../utils/constants';
|
||||||
|
|
||||||
|
const nestedListConfig = {
|
||||||
|
collections: [
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
name: 'settings',
|
||||||
|
label: 'Settings',
|
||||||
|
editor: { preview: false },
|
||||||
|
files: [
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
name: 'hotel_locations',
|
||||||
|
label: 'Hotel Locations',
|
||||||
|
file: '_data/hotel_locations.yml',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Country',
|
||||||
|
name: 'country',
|
||||||
|
widget: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Hotel Locations',
|
||||||
|
name: 'hotel_locations',
|
||||||
|
widget: 'list',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Cities',
|
||||||
|
name: 'cities',
|
||||||
|
widget: 'list',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'City',
|
||||||
|
name: 'city',
|
||||||
|
widget: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Number of Hotels in City',
|
||||||
|
name: 'number_of_hotels_in_city',
|
||||||
|
widget: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'City Locations',
|
||||||
|
name: 'city_locations',
|
||||||
|
widget: 'list',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Hotel Name',
|
||||||
|
name: 'hotel_name',
|
||||||
|
widget: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Test Backend Editorial Workflow', () => {
|
||||||
|
after(() => {
|
||||||
|
cy.task('teardownBackend', { backend: 'test' });
|
||||||
|
});
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
Cypress.config('defaultCommandTimeout', 4000);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.task('setupBackend', { backend: 'test' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can validate object fields', () => {
|
||||||
|
login();
|
||||||
|
validateObjectFieldsAndExit(setting1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can validate fields nested in an object field', () => {
|
||||||
|
login();
|
||||||
|
validateNestedObjectFieldsAndExit(setting1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can validate list fields', () => {
|
||||||
|
login();
|
||||||
|
validateListFieldsAndExit(setting2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can validate deeply nested list fields', () => {
|
||||||
|
cy.task('updateConfig', nestedListConfig);
|
||||||
|
|
||||||
|
login();
|
||||||
|
validateNestedListFieldsAndExit(setting2);
|
||||||
|
});
|
||||||
|
});
|
@ -3,6 +3,10 @@ const editorStatus = { draft: 'Draft', review: 'In review', ready: 'Ready' };
|
|||||||
const setting1 = { limit: 10, author: 'John Doe' };
|
const setting1 = { limit: 10, author: 'John Doe' };
|
||||||
const setting2 = { name: 'Jane Doe', description: 'description' };
|
const setting2 = { name: 'Jane Doe', description: 'description' };
|
||||||
const publishTypes = { publishNow: 'Publish now', publishAndCreateNew: 'Publish and create new', publishAndDuplicate: 'Publish and duplicate' };
|
const publishTypes = { publishNow: 'Publish now', publishAndCreateNew: 'Publish and create new', publishAndDuplicate: 'Publish and duplicate' };
|
||||||
|
const colorError = 'rgb(255, 0, 59)';
|
||||||
|
const colorNormal = 'rgb(223, 223, 227)';
|
||||||
|
const textColorNormal = 'rgb(68, 74, 87)';
|
||||||
|
|
||||||
const notifications = {
|
const notifications = {
|
||||||
saved: 'Entry saved',
|
saved: 'Entry saved',
|
||||||
published: 'Entry published',
|
published: 'Entry published',
|
||||||
@ -40,5 +44,8 @@ module.exports = {
|
|||||||
setting2,
|
setting2,
|
||||||
notifications,
|
notifications,
|
||||||
publishTypes,
|
publishTypes,
|
||||||
HOT_KEY_MAP
|
HOT_KEY_MAP,
|
||||||
|
colorError,
|
||||||
|
colorNormal,
|
||||||
|
textColorNormal,
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
const { notifications, workflowStatus, editorStatus, publishTypes } = require('./constants');
|
const {
|
||||||
|
notifications,
|
||||||
|
workflowStatus,
|
||||||
|
editorStatus,
|
||||||
|
publishTypes,
|
||||||
|
colorError,
|
||||||
|
colorNormal,
|
||||||
|
textColorNormal,
|
||||||
|
} = require('./constants');
|
||||||
|
|
||||||
function login(user) {
|
function login(user) {
|
||||||
cy.viewport(1200, 1200);
|
cy.viewport(1200, 1200);
|
||||||
@ -38,6 +46,35 @@ function assertNotification(message) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertColorOn(cssProperty, color, opts) {
|
||||||
|
if (opts.type && opts.type === 'label') {
|
||||||
|
(opts.scope ? opts.scope : cy).contains('label', opts.label).should($el => {
|
||||||
|
expect($el).to.have.css(cssProperty, color);
|
||||||
|
});
|
||||||
|
} else if (opts.type && opts.type === 'field') {
|
||||||
|
const assertion = $el => expect($el).to.have.css(cssProperty, color);
|
||||||
|
if (opts.isMarkdown) {
|
||||||
|
(opts.scope ? opts.scope : cy)
|
||||||
|
.contains('label', opts.label)
|
||||||
|
.next()
|
||||||
|
.children()
|
||||||
|
.eq(0)
|
||||||
|
.children()
|
||||||
|
.eq(1)
|
||||||
|
.should(assertion);
|
||||||
|
} else {
|
||||||
|
(opts.scope ? opts.scope : cy)
|
||||||
|
.contains('label', opts.label)
|
||||||
|
.next()
|
||||||
|
.should(assertion);
|
||||||
|
}
|
||||||
|
} else if (opts.el) {
|
||||||
|
opts.el.should($el => {
|
||||||
|
expect($el).to.have.css(cssProperty, color);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function exitEditor() {
|
function exitEditor() {
|
||||||
cy.contains('a[href^="#/collections/"]', 'Writing in').click();
|
cy.contains('a[href^="#/collections/"]', 'Writing in').click();
|
||||||
}
|
}
|
||||||
@ -204,9 +241,6 @@ function flushClockAndSave() {
|
|||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
cy.get('input')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
cy.contains('button', 'Save').click();
|
cy.contains('button', 'Save').click();
|
||||||
assertNotification(notifications.saved);
|
assertNotification(notifications.saved);
|
||||||
});
|
});
|
||||||
@ -381,10 +415,12 @@ function validateObjectFields({ limit, author }) {
|
|||||||
cy.get('input[type=number]').type(limit);
|
cy.get('input[type=number]').type(limit);
|
||||||
cy.contains('button', 'Save').click();
|
cy.contains('button', 'Save').click();
|
||||||
assertNotification(notifications.error.missingField);
|
assertNotification(notifications.error.missingField);
|
||||||
|
assertFieldErrorStatus('Default Author', colorError);
|
||||||
cy.contains('label', 'Default Author').click();
|
cy.contains('label', 'Default Author').click();
|
||||||
cy.focused().type(author);
|
cy.focused().type(author);
|
||||||
cy.contains('button', 'Save').click();
|
cy.contains('button', 'Save').click();
|
||||||
assertNotification(notifications.saved);
|
assertNotification(notifications.saved);
|
||||||
|
assertFieldErrorStatus('Default Author', colorNormal);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateNestedObjectFields({ limit, author }) {
|
function validateNestedObjectFields({ limit, author }) {
|
||||||
@ -415,13 +451,126 @@ function validateListFields({ name, description }) {
|
|||||||
cy.contains('button', 'Add').click();
|
cy.contains('button', 'Add').click();
|
||||||
cy.contains('button', 'Save').click();
|
cy.contains('button', 'Save').click();
|
||||||
assertNotification(notifications.error.missingField);
|
assertNotification(notifications.error.missingField);
|
||||||
|
assertFieldErrorStatus('Authors', colorError);
|
||||||
|
cy.get('div[class*=ListControl]')
|
||||||
|
.eq(2)
|
||||||
|
.as('listControl');
|
||||||
|
assertFieldErrorStatus('Name', colorError, { scope: cy.get('@listControl') });
|
||||||
|
assertColorOn('background-color', colorError, {
|
||||||
|
type: 'label',
|
||||||
|
label: 'Description',
|
||||||
|
scope: cy.get('@listControl'),
|
||||||
|
isMarkdown: true,
|
||||||
|
});
|
||||||
|
assertListControlErrorStatus([colorError, colorError], '@listControl');
|
||||||
cy.get('input')
|
cy.get('input')
|
||||||
.eq(2)
|
.eq(2)
|
||||||
.type(name);
|
.type(name);
|
||||||
cy.getMarkdownEditor()
|
cy.getMarkdownEditor()
|
||||||
.eq(2)
|
.eq(2)
|
||||||
.type(description);
|
.type(description);
|
||||||
|
flushClockAndSave();
|
||||||
|
assertNotification(notifications.saved);
|
||||||
|
assertFieldErrorStatus('Authors', colorNormal);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateNestedListFields() {
|
||||||
|
cy.get('a[href^="#/collections/settings"]').click();
|
||||||
|
cy.get('a[href^="#/collections/settings/entries/hotel_locations"]').click();
|
||||||
|
|
||||||
|
// add first city list item
|
||||||
|
cy.contains('button', 'hotel locations').click();
|
||||||
|
cy.contains('button', 'cities').click();
|
||||||
|
cy.contains('label', 'City')
|
||||||
|
.next()
|
||||||
|
.type('Washington DC');
|
||||||
|
cy.contains('label', 'Number of Hotels in City')
|
||||||
|
.next()
|
||||||
|
.type('5');
|
||||||
|
cy.contains('button', 'city locations').click();
|
||||||
|
|
||||||
|
// add second city list item
|
||||||
|
cy.contains('button', 'cities').click();
|
||||||
|
cy.contains('label', 'Cities')
|
||||||
|
.next()
|
||||||
|
.find('div[class*=ListControl]')
|
||||||
|
.eq(2)
|
||||||
|
.as('secondCitiesListControl');
|
||||||
|
cy.get('@secondCitiesListControl')
|
||||||
|
.contains('label', 'City')
|
||||||
|
.next()
|
||||||
|
.type('Boston');
|
||||||
|
cy.get('@secondCitiesListControl')
|
||||||
|
.contains('button', 'city locations')
|
||||||
|
.click();
|
||||||
|
|
||||||
cy.contains('button', 'Save').click();
|
cy.contains('button', 'Save').click();
|
||||||
|
assertNotification(notifications.error.missingField);
|
||||||
|
|
||||||
|
// assert on fields
|
||||||
|
assertFieldErrorStatus('Hotel Locations', colorError);
|
||||||
|
assertFieldErrorStatus('Cities', colorError);
|
||||||
|
assertFieldErrorStatus('City', colorNormal);
|
||||||
|
assertFieldErrorStatus('City', colorNormal, { scope: cy.get('@secondCitiesListControl') });
|
||||||
|
assertFieldErrorStatus('Number of Hotels in City', colorNormal);
|
||||||
|
assertFieldErrorStatus('Number of Hotels in City', colorError, {
|
||||||
|
scope: cy.get('@secondCitiesListControl'),
|
||||||
|
});
|
||||||
|
assertFieldErrorStatus('City Locations', colorError);
|
||||||
|
assertFieldErrorStatus('City Locations', colorError, {
|
||||||
|
scope: cy.get('@secondCitiesListControl'),
|
||||||
|
});
|
||||||
|
assertFieldErrorStatus('Hotel Name', colorError);
|
||||||
|
assertFieldErrorStatus('Hotel Name', colorError, { scope: cy.get('@secondCitiesListControl') });
|
||||||
|
|
||||||
|
// list control aliases
|
||||||
|
cy.contains('label', 'Hotel Locations')
|
||||||
|
.next()
|
||||||
|
.find('div[class*=ListControl]')
|
||||||
|
.first()
|
||||||
|
.as('hotelLocationsListControl');
|
||||||
|
cy.contains('label', 'Cities')
|
||||||
|
.next()
|
||||||
|
.find('div[class*=ListControl]')
|
||||||
|
.eq(0)
|
||||||
|
.as('firstCitiesListControl');
|
||||||
|
cy.contains('label', 'City Locations')
|
||||||
|
.next()
|
||||||
|
.find('div[class*=ListControl]')
|
||||||
|
.eq(0)
|
||||||
|
.as('firstCityLocationsListControl');
|
||||||
|
cy.contains('label', 'Cities')
|
||||||
|
.next()
|
||||||
|
.find('div[class*=ListControl]')
|
||||||
|
.eq(3)
|
||||||
|
.as('secondCityLocationsListControl');
|
||||||
|
|
||||||
|
// assert on list controls
|
||||||
|
assertListControlErrorStatus([colorError, colorError], '@hotelLocationsListControl');
|
||||||
|
assertListControlErrorStatus([colorError, colorError], '@firstCitiesListControl');
|
||||||
|
assertListControlErrorStatus([colorError, colorError], '@secondCitiesListControl');
|
||||||
|
assertListControlErrorStatus([colorError, colorError], '@firstCityLocationsListControl');
|
||||||
|
assertListControlErrorStatus([colorError, colorError], '@secondCityLocationsListControl');
|
||||||
|
|
||||||
|
cy.contains('label', 'Hotel Name')
|
||||||
|
.next()
|
||||||
|
.type('The Ritz Carlton');
|
||||||
|
cy.contains('button', 'Save').click();
|
||||||
|
assertNotification(notifications.error.missingField);
|
||||||
|
assertListControlErrorStatus([colorNormal, textColorNormal], '@firstCitiesListControl');
|
||||||
|
|
||||||
|
// fill out rest of form and save
|
||||||
|
cy.get('@secondCitiesListControl')
|
||||||
|
.contains('label', 'Number of Hotels in City')
|
||||||
|
.type(3);
|
||||||
|
cy.get('@secondCitiesListControl')
|
||||||
|
.contains('label', 'Hotel Name')
|
||||||
|
.type('Grand Hyatt');
|
||||||
|
cy.contains('label', 'Country')
|
||||||
|
.next()
|
||||||
|
.type('United States');
|
||||||
|
flushClockAndSave();
|
||||||
|
assertNotification(notifications.saved);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateObjectFieldsAndExit(setting) {
|
function validateObjectFieldsAndExit(setting) {
|
||||||
@ -439,10 +588,53 @@ function validateListFieldsAndExit(setting) {
|
|||||||
exitEditor();
|
exitEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateNestedListFieldsAndExit(setting) {
|
||||||
|
validateNestedListFields(setting);
|
||||||
|
exitEditor();
|
||||||
|
}
|
||||||
|
|
||||||
function assertFieldValidationError({ message, fieldLabel }) {
|
function assertFieldValidationError({ message, fieldLabel }) {
|
||||||
cy.contains('label', fieldLabel)
|
cy.contains('label', fieldLabel)
|
||||||
.siblings('ul[class*=ControlErrorsList]')
|
.siblings('ul[class*=ControlErrorsList]')
|
||||||
.contains(message);
|
.contains(message);
|
||||||
|
assertFieldErrorStatus(fieldLabel, colorError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertFieldErrorStatus(label, color, opts = { isMarkdown: false }) {
|
||||||
|
assertColorOn('background-color', color, {
|
||||||
|
type: 'label',
|
||||||
|
label,
|
||||||
|
scope: opts.scope,
|
||||||
|
isMarkdown: opts.isMarkdown,
|
||||||
|
});
|
||||||
|
assertColorOn('border-color', color, {
|
||||||
|
type: 'field',
|
||||||
|
label,
|
||||||
|
scope: opts.scope,
|
||||||
|
isMarkdown: opts.isMarkdown,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertListControlErrorStatus(colors = ['', ''], alias) {
|
||||||
|
cy.get(alias).within(() => {
|
||||||
|
// assert list item border has correct color
|
||||||
|
assertColorOn('border-right-color', colors[0], {
|
||||||
|
el: cy
|
||||||
|
.root()
|
||||||
|
.children()
|
||||||
|
.eq(2),
|
||||||
|
});
|
||||||
|
// collapse list item
|
||||||
|
cy.get('button[class*=TopBarButton-button]')
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
// assert list item label text has correct color
|
||||||
|
assertColorOn('color', colors[1], { el: cy.get('div[class*=NestedObjectLabel]').first() });
|
||||||
|
// uncollapse list item
|
||||||
|
cy.get('button[class*=TopBarButton-button]')
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@ -468,6 +660,7 @@ module.exports = {
|
|||||||
validateObjectFieldsAndExit,
|
validateObjectFieldsAndExit,
|
||||||
validateNestedObjectFieldsAndExit,
|
validateNestedObjectFieldsAndExit,
|
||||||
validateListFieldsAndExit,
|
validateListFieldsAndExit,
|
||||||
|
validateNestedListFieldsAndExit,
|
||||||
unpublishEntry,
|
unpublishEntry,
|
||||||
publishEntryInEditor,
|
publishEntryInEditor,
|
||||||
duplicateEntry,
|
duplicateEntry,
|
||||||
|
@ -349,7 +349,7 @@ export function changeDraftField(
|
|||||||
|
|
||||||
export function changeDraftFieldValidation(
|
export function changeDraftFieldValidation(
|
||||||
uniquefieldId: string,
|
uniquefieldId: string,
|
||||||
errors: { type: string; message: string }[],
|
errors: { type: string; parentIds: string[]; message: string }[],
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
type: DRAFT_VALIDATION_ERRORS,
|
type: DRAFT_VALIDATION_ERRORS,
|
||||||
|
@ -115,6 +115,11 @@ class EditorControl extends React.Component {
|
|||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
isEditorComponent: PropTypes.bool,
|
isEditorComponent: PropTypes.bool,
|
||||||
isNewEditorComponent: PropTypes.bool,
|
isNewEditorComponent: PropTypes.bool,
|
||||||
|
parentIds: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
parentIds: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -123,6 +128,17 @@ class EditorControl extends React.Component {
|
|||||||
|
|
||||||
uniqueFieldId = uniqueId(`${this.props.field.get('name')}-field-`);
|
uniqueFieldId = uniqueId(`${this.props.field.get('name')}-field-`);
|
||||||
|
|
||||||
|
isAncestorOfFieldError = () => {
|
||||||
|
const { fieldsErrors } = this.props;
|
||||||
|
|
||||||
|
if (fieldsErrors && fieldsErrors.size > 0) {
|
||||||
|
return Object.values(fieldsErrors.toJS()).some(arr =>
|
||||||
|
arr.some(err => err.parentIds && err.parentIds.includes(this.uniqueFieldId)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
value,
|
value,
|
||||||
@ -153,6 +169,7 @@ class EditorControl extends React.Component {
|
|||||||
isSelected,
|
isSelected,
|
||||||
isEditorComponent,
|
isEditorComponent,
|
||||||
isNewEditorComponent,
|
isNewEditorComponent,
|
||||||
|
parentIds,
|
||||||
t,
|
t,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -164,6 +181,9 @@ class EditorControl extends React.Component {
|
|||||||
const onValidateObject = onValidate;
|
const onValidateObject = onValidate;
|
||||||
const metadata = fieldsMetaData && fieldsMetaData.get(fieldName);
|
const metadata = fieldsMetaData && fieldsMetaData.get(fieldName);
|
||||||
const errors = fieldsErrors && fieldsErrors.get(this.uniqueFieldId);
|
const errors = fieldsErrors && fieldsErrors.get(this.uniqueFieldId);
|
||||||
|
const childErrors = this.isAncestorOfFieldError();
|
||||||
|
const hasErrors = !!errors || childErrors;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClassNames>
|
<ClassNames>
|
||||||
{({ css, cx }) => (
|
{({ css, cx }) => (
|
||||||
@ -184,7 +204,7 @@ class EditorControl extends React.Component {
|
|||||||
)}
|
)}
|
||||||
<FieldLabel
|
<FieldLabel
|
||||||
isActive={isSelected || this.state.styleActive}
|
isActive={isSelected || this.state.styleActive}
|
||||||
hasErrors={!!errors}
|
hasErrors={hasErrors}
|
||||||
htmlFor={this.uniqueFieldId}
|
htmlFor={this.uniqueFieldId}
|
||||||
>
|
>
|
||||||
{`${field.get('label', field.get('name'))}${
|
{`${field.get('label', field.get('name'))}${
|
||||||
@ -204,7 +224,7 @@ class EditorControl extends React.Component {
|
|||||||
{
|
{
|
||||||
[css`
|
[css`
|
||||||
${styleStrings.widgetError};
|
${styleStrings.widgetError};
|
||||||
`]: !!errors,
|
`]: hasErrors,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
classNameWidget={css`
|
classNameWidget={css`
|
||||||
@ -255,10 +275,11 @@ class EditorControl extends React.Component {
|
|||||||
onValidateObject={onValidateObject}
|
onValidateObject={onValidateObject}
|
||||||
isEditorComponent={isEditorComponent}
|
isEditorComponent={isEditorComponent}
|
||||||
isNewEditorComponent={isNewEditorComponent}
|
isNewEditorComponent={isNewEditorComponent}
|
||||||
|
parentIds={parentIds}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
{fieldHint && (
|
{fieldHint && (
|
||||||
<ControlHint active={isSelected || this.state.styleActive} error={!!errors}>
|
<ControlHint active={isSelected || this.state.styleActive} error={hasErrors}>
|
||||||
{fieldHint}
|
{fieldHint}
|
||||||
</ControlHint>
|
</ControlHint>
|
||||||
)}
|
)}
|
||||||
|
@ -118,11 +118,12 @@ export default class Widget extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
validatePresence = (field, value) => {
|
validatePresence = (field, value) => {
|
||||||
const t = this.props.t;
|
const { t, parentIds } = this.props;
|
||||||
const isRequired = field.get('required', true);
|
const isRequired = field.get('required', true);
|
||||||
if (isRequired && isEmpty(value)) {
|
if (isRequired && isEmpty(value)) {
|
||||||
const error = {
|
const error = {
|
||||||
type: ValidationErrorTypes.PRESENCE,
|
type: ValidationErrorTypes.PRESENCE,
|
||||||
|
parentIds,
|
||||||
message: t('editor.editorControlPane.widget.required', {
|
message: t('editor.editorControlPane.widget.required', {
|
||||||
fieldLabel: field.get('label', field.get('name')),
|
fieldLabel: field.get('label', field.get('name')),
|
||||||
}),
|
}),
|
||||||
@ -134,7 +135,7 @@ export default class Widget extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
validatePattern = (field, value) => {
|
validatePattern = (field, value) => {
|
||||||
const t = this.props.t;
|
const { t, parentIds } = this.props;
|
||||||
const pattern = field.get('pattern', false);
|
const pattern = field.get('pattern', false);
|
||||||
|
|
||||||
if (isEmpty(value)) {
|
if (isEmpty(value)) {
|
||||||
@ -144,6 +145,7 @@ export default class Widget extends Component {
|
|||||||
if (pattern && !RegExp(pattern.first()).test(value)) {
|
if (pattern && !RegExp(pattern.first()).test(value)) {
|
||||||
const error = {
|
const error = {
|
||||||
type: ValidationErrorTypes.PATTERN,
|
type: ValidationErrorTypes.PATTERN,
|
||||||
|
parentIds,
|
||||||
message: t('editor.editorControlPane.widget.regexPattern', {
|
message: t('editor.editorControlPane.widget.regexPattern', {
|
||||||
fieldLabel: field.get('label', field.get('name')),
|
fieldLabel: field.get('label', field.get('name')),
|
||||||
pattern: pattern.last(),
|
pattern: pattern.last(),
|
||||||
@ -157,7 +159,7 @@ export default class Widget extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
validateWrappedControl = field => {
|
validateWrappedControl = field => {
|
||||||
const t = this.props.t;
|
const { t, parentIds } = this.props;
|
||||||
if (typeof this.wrappedControlValid !== 'function') {
|
if (typeof this.wrappedControlValid !== 'function') {
|
||||||
throw new Error(oneLine`
|
throw new Error(oneLine`
|
||||||
this.wrappedControlValid is not a function. Are you sure widget
|
this.wrappedControlValid is not a function. Are you sure widget
|
||||||
@ -188,6 +190,7 @@ export default class Widget extends Component {
|
|||||||
|
|
||||||
const error = {
|
const error = {
|
||||||
type: ValidationErrorTypes.CUSTOM,
|
type: ValidationErrorTypes.CUSTOM,
|
||||||
|
parentIds,
|
||||||
message: t('editor.editorControlPane.widget.processing', {
|
message: t('editor.editorControlPane.widget.processing', {
|
||||||
fieldLabel: field.get('label', field.get('name')),
|
fieldLabel: field.get('label', field.get('name')),
|
||||||
}),
|
}),
|
||||||
@ -257,6 +260,7 @@ export default class Widget extends Component {
|
|||||||
controlRef,
|
controlRef,
|
||||||
isEditorComponent,
|
isEditorComponent,
|
||||||
isNewEditorComponent,
|
isNewEditorComponent,
|
||||||
|
parentIds,
|
||||||
t,
|
t,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return React.createElement(controlComponent, {
|
return React.createElement(controlComponent, {
|
||||||
@ -301,6 +305,7 @@ export default class Widget extends Component {
|
|||||||
isNewEditorComponent,
|
isNewEditorComponent,
|
||||||
fieldsErrors,
|
fieldsErrors,
|
||||||
controlRef,
|
controlRef,
|
||||||
|
parentIds,
|
||||||
t,
|
t,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -110,6 +110,7 @@ export default class ListControl extends React.Component {
|
|||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
value: List(),
|
value: List(),
|
||||||
|
parentIds: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -410,6 +411,15 @@ export default class ListControl extends React.Component {
|
|||||||
this.validations = this.validations.filter(item => updatedKeys.includes(item.key));
|
this.validations = this.validations.filter(item => updatedKeys.includes(item.key));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
hasError = index => {
|
||||||
|
const { fieldsErrors } = this.props;
|
||||||
|
if (fieldsErrors && fieldsErrors.size > 0) {
|
||||||
|
return Object.values(fieldsErrors.toJS()).some(arr =>
|
||||||
|
arr.some(err => err.parentIds && err.parentIds.includes(this.state.keys[index])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line react/display-name
|
// eslint-disable-next-line react/display-name
|
||||||
renderItem = (item, index) => {
|
renderItem = (item, index) => {
|
||||||
const {
|
const {
|
||||||
@ -421,12 +431,15 @@ export default class ListControl extends React.Component {
|
|||||||
fieldsErrors,
|
fieldsErrors,
|
||||||
controlRef,
|
controlRef,
|
||||||
resolveWidget,
|
resolveWidget,
|
||||||
|
parentIds,
|
||||||
|
forID,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { itemsCollapsed, keys } = this.state;
|
const { itemsCollapsed, keys } = this.state;
|
||||||
const collapsed = itemsCollapsed[index];
|
const collapsed = itemsCollapsed[index];
|
||||||
const key = keys[index];
|
const key = keys[index];
|
||||||
let field = this.props.field;
|
let field = this.props.field;
|
||||||
|
const hasError = this.hasError(index);
|
||||||
|
|
||||||
if (this.getValueType() === valueTypes.MIXED) {
|
if (this.getValueType() === valueTypes.MIXED) {
|
||||||
field = getTypedFieldForValue(field, item);
|
field = getTypedFieldForValue(field, item);
|
||||||
@ -448,7 +461,9 @@ export default class ListControl extends React.Component {
|
|||||||
dragHandleHOC={SortableHandle}
|
dragHandleHOC={SortableHandle}
|
||||||
data-testid={`styled-list-item-top-bar-${key}`}
|
data-testid={`styled-list-item-top-bar-${key}`}
|
||||||
/>
|
/>
|
||||||
<NestedObjectLabel collapsed={collapsed}>{this.objectLabel(item)}</NestedObjectLabel>
|
<NestedObjectLabel collapsed={collapsed} error={hasError}>
|
||||||
|
{this.objectLabel(item)}
|
||||||
|
</NestedObjectLabel>
|
||||||
<ClassNames>
|
<ClassNames>
|
||||||
{({ css, cx }) => (
|
{({ css, cx }) => (
|
||||||
<ObjectControl
|
<ObjectControl
|
||||||
@ -472,6 +487,8 @@ export default class ListControl extends React.Component {
|
|||||||
validationKey={key}
|
validationKey={key}
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
data-testid={`object-control-${key}`}
|
data-testid={`object-control-${key}`}
|
||||||
|
hasError={hasError}
|
||||||
|
parentIds={[...parentIds, forID, key]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ClassNames>
|
</ClassNames>
|
||||||
|
@ -52,6 +52,7 @@ describe('ListControl', () => {
|
|||||||
entry: fromJS({
|
entry: fromJS({
|
||||||
path: 'posts/index.md',
|
path: 'posts/index.md',
|
||||||
}),
|
}),
|
||||||
|
forID: 'forID',
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -148,6 +148,7 @@ exports[`ListControl should add to list when add button is clicked 1`] = `
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="classNameWrapper emotion-18"
|
class="classNameWrapper emotion-18"
|
||||||
|
id="forID"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="emotion-12 emotion-13"
|
class="emotion-12 emotion-13"
|
||||||
@ -218,6 +219,7 @@ exports[`ListControl should add to list when add button is clicked 1`] = `
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,0"
|
||||||
validationkey="0"
|
validationkey="0"
|
||||||
value="Map {}"
|
value="Map {}"
|
||||||
/>
|
/>
|
||||||
@ -375,6 +377,7 @@ exports[`ListControl should remove from list when remove button is clicked 1`] =
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="classNameWrapper emotion-22"
|
class="classNameWrapper emotion-22"
|
||||||
|
id="forID"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="emotion-12 emotion-13"
|
class="emotion-12 emotion-13"
|
||||||
@ -445,6 +448,7 @@ exports[`ListControl should remove from list when remove button is clicked 1`] =
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"minimize_collapsed\\": true, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"minimize_collapsed\\": true, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,0"
|
||||||
validationkey="0"
|
validationkey="0"
|
||||||
value="Map { \\"string\\": \\"item 1\\" }"
|
value="Map { \\"string\\": \\"item 1\\" }"
|
||||||
/>
|
/>
|
||||||
@ -473,6 +477,7 @@ exports[`ListControl should remove from list when remove button is clicked 1`] =
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"minimize_collapsed\\": true, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"minimize_collapsed\\": true, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,1"
|
||||||
validationkey="1"
|
validationkey="1"
|
||||||
value="Map { \\"string\\": \\"item 2\\" }"
|
value="Map { \\"string\\": \\"item 2\\" }"
|
||||||
/>
|
/>
|
||||||
@ -631,6 +636,7 @@ exports[`ListControl should remove from list when remove button is clicked 2`] =
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="classNameWrapper emotion-18"
|
class="classNameWrapper emotion-18"
|
||||||
|
id="forID"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="emotion-12 emotion-13"
|
class="emotion-12 emotion-13"
|
||||||
@ -701,6 +707,7 @@ exports[`ListControl should remove from list when remove button is clicked 2`] =
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"minimize_collapsed\\": true, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"minimize_collapsed\\": true, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,1"
|
||||||
validationkey="1"
|
validationkey="1"
|
||||||
value="Map { \\"string\\": \\"item 2\\" }"
|
value="Map { \\"string\\": \\"item 2\\" }"
|
||||||
/>
|
/>
|
||||||
@ -714,6 +721,7 @@ exports[`ListControl should render empty list 1`] = `
|
|||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<input
|
<input
|
||||||
class="classNameWrapper"
|
class="classNameWrapper"
|
||||||
|
id="forID"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
@ -868,6 +876,7 @@ exports[`ListControl should render list with fields with collapse = "false" and
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="classNameWrapper emotion-22"
|
class="classNameWrapper emotion-22"
|
||||||
|
id="forID"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="emotion-12 emotion-13"
|
class="emotion-12 emotion-13"
|
||||||
@ -938,6 +947,7 @@ exports[`ListControl should render list with fields with collapse = "false" and
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,0"
|
||||||
validationkey="0"
|
validationkey="0"
|
||||||
value="Map { \\"string\\": \\"item 1\\" }"
|
value="Map { \\"string\\": \\"item 1\\" }"
|
||||||
/>
|
/>
|
||||||
@ -966,6 +976,7 @@ exports[`ListControl should render list with fields with collapse = "false" and
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,1"
|
||||||
validationkey="1"
|
validationkey="1"
|
||||||
value="Map { \\"string\\": \\"item 2\\" }"
|
value="Map { \\"string\\": \\"item 2\\" }"
|
||||||
/>
|
/>
|
||||||
@ -1123,6 +1134,7 @@ exports[`ListControl should render list with fields with collapse = "false" and
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="classNameWrapper emotion-22"
|
class="classNameWrapper emotion-22"
|
||||||
|
id="forID"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="emotion-12 emotion-13"
|
class="emotion-12 emotion-13"
|
||||||
@ -1193,6 +1205,7 @@ exports[`ListControl should render list with fields with collapse = "false" and
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"minimize_collapsed\\": true, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"minimize_collapsed\\": true, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,0"
|
||||||
validationkey="0"
|
validationkey="0"
|
||||||
value="Map { \\"string\\": \\"item 1\\" }"
|
value="Map { \\"string\\": \\"item 1\\" }"
|
||||||
/>
|
/>
|
||||||
@ -1221,6 +1234,7 @@ exports[`ListControl should render list with fields with collapse = "false" and
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"minimize_collapsed\\": true, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"minimize_collapsed\\": true, \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,1"
|
||||||
validationkey="1"
|
validationkey="1"
|
||||||
value="Map { \\"string\\": \\"item 2\\" }"
|
value="Map { \\"string\\": \\"item 2\\" }"
|
||||||
/>
|
/>
|
||||||
@ -1379,6 +1393,7 @@ exports[`ListControl should render list with fields with default collapse ("true
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="classNameWrapper emotion-22"
|
class="classNameWrapper emotion-22"
|
||||||
|
id="forID"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="emotion-12 emotion-13"
|
class="emotion-12 emotion-13"
|
||||||
@ -1449,6 +1464,7 @@ exports[`ListControl should render list with fields with default collapse ("true
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,0"
|
||||||
validationkey="0"
|
validationkey="0"
|
||||||
value="Map { \\"string\\": \\"item 1\\" }"
|
value="Map { \\"string\\": \\"item 1\\" }"
|
||||||
/>
|
/>
|
||||||
@ -1477,6 +1493,7 @@ exports[`ListControl should render list with fields with default collapse ("true
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"fields\\": List [ Map { \\"label\\": \\"String\\", \\"name\\": \\"string\\", \\"widget\\": \\"string\\" } ] }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,1"
|
||||||
validationkey="1"
|
validationkey="1"
|
||||||
value="Map { \\"string\\": \\"item 2\\" }"
|
value="Map { \\"string\\": \\"item 2\\" }"
|
||||||
/>
|
/>
|
||||||
@ -1617,6 +1634,7 @@ exports[`ListControl should render list with fields with default collapse ("true
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="classNameWrapper emotion-14"
|
class="classNameWrapper emotion-14"
|
||||||
|
id="forID"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="emotion-12 emotion-13"
|
class="emotion-12 emotion-13"
|
||||||
@ -1815,6 +1833,7 @@ exports[`ListControl should render list with nested object 1`] = `
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="classNameWrapper emotion-22"
|
class="classNameWrapper emotion-22"
|
||||||
|
id="forID"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="emotion-12 emotion-13"
|
class="emotion-12 emotion-13"
|
||||||
@ -1885,6 +1904,7 @@ exports[`ListControl should render list with nested object 1`] = `
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"field\\": Map { \\"name\\": \\"object\\", \\"widget\\": \\"object\\", \\"label\\": \\"Object\\", \\"fields\\": List [ Map { \\"name\\": \\"title\\", \\"widget\\": \\"string\\", \\"label\\": \\"Title\\" } ] } }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"field\\": Map { \\"name\\": \\"object\\", \\"widget\\": \\"object\\", \\"label\\": \\"Object\\", \\"fields\\": List [ Map { \\"name\\": \\"title\\", \\"widget\\": \\"string\\", \\"label\\": \\"Title\\" } ] } }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,0"
|
||||||
validationkey="0"
|
validationkey="0"
|
||||||
value="Map { \\"object\\": Map { \\"title\\": \\"item 1\\" } }"
|
value="Map { \\"object\\": Map { \\"title\\": \\"item 1\\" } }"
|
||||||
/>
|
/>
|
||||||
@ -1913,6 +1933,7 @@ exports[`ListControl should render list with nested object 1`] = `
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"field\\": Map { \\"name\\": \\"object\\", \\"widget\\": \\"object\\", \\"label\\": \\"Object\\", \\"fields\\": List [ Map { \\"name\\": \\"title\\", \\"widget\\": \\"string\\", \\"label\\": \\"Title\\" } ] } }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"field\\": Map { \\"name\\": \\"object\\", \\"widget\\": \\"object\\", \\"label\\": \\"Object\\", \\"fields\\": List [ Map { \\"name\\": \\"title\\", \\"widget\\": \\"string\\", \\"label\\": \\"Title\\" } ] } }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,1"
|
||||||
validationkey="1"
|
validationkey="1"
|
||||||
value="Map { \\"object\\": Map { \\"title\\": \\"item 2\\" } }"
|
value="Map { \\"object\\": Map { \\"title\\": \\"item 2\\" } }"
|
||||||
/>
|
/>
|
||||||
@ -2070,6 +2091,7 @@ exports[`ListControl should render list with nested object with collapse = false
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="classNameWrapper emotion-22"
|
class="classNameWrapper emotion-22"
|
||||||
|
id="forID"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="emotion-12 emotion-13"
|
class="emotion-12 emotion-13"
|
||||||
@ -2140,6 +2162,7 @@ exports[`ListControl should render list with nested object with collapse = false
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"field\\": Map { \\"name\\": \\"object\\", \\"widget\\": \\"object\\", \\"label\\": \\"Object\\", \\"fields\\": List [ Map { \\"name\\": \\"title\\", \\"widget\\": \\"string\\", \\"label\\": \\"Title\\" } ] } }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"field\\": Map { \\"name\\": \\"object\\", \\"widget\\": \\"object\\", \\"label\\": \\"Object\\", \\"fields\\": List [ Map { \\"name\\": \\"title\\", \\"widget\\": \\"string\\", \\"label\\": \\"Title\\" } ] } }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,0"
|
||||||
validationkey="0"
|
validationkey="0"
|
||||||
value="Map { \\"object\\": Map { \\"title\\": \\"item 1\\" } }"
|
value="Map { \\"object\\": Map { \\"title\\": \\"item 1\\" } }"
|
||||||
/>
|
/>
|
||||||
@ -2168,6 +2191,7 @@ exports[`ListControl should render list with nested object with collapse = false
|
|||||||
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"field\\": Map { \\"name\\": \\"object\\", \\"widget\\": \\"object\\", \\"label\\": \\"Object\\", \\"fields\\": List [ Map { \\"name\\": \\"title\\", \\"widget\\": \\"string\\", \\"label\\": \\"Title\\" } ] } }"
|
field="Map { \\"name\\": \\"list\\", \\"label\\": \\"List\\", \\"collapsed\\": false, \\"field\\": Map { \\"name\\": \\"object\\", \\"widget\\": \\"object\\", \\"label\\": \\"Object\\", \\"fields\\": List [ Map { \\"name\\": \\"title\\", \\"widget\\": \\"string\\", \\"label\\": \\"Title\\" } ] } }"
|
||||||
fieldserrors="Map {}"
|
fieldserrors="Map {}"
|
||||||
forlist="true"
|
forlist="true"
|
||||||
|
parentids="forID,1"
|
||||||
validationkey="1"
|
validationkey="1"
|
||||||
value="Map { \\"object\\": Map { \\"title\\": \\"item 2\\" } }"
|
value="Map { \\"object\\": Map { \\"title\\": \\"item 2\\" } }"
|
||||||
/>
|
/>
|
||||||
@ -2181,6 +2205,7 @@ exports[`ListControl should render list with string array 1`] = `
|
|||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<input
|
<input
|
||||||
class="classNameWrapper"
|
class="classNameWrapper"
|
||||||
|
id="forID"
|
||||||
type="text"
|
type="text"
|
||||||
value="item 1, item 2"
|
value="item 1, item 2"
|
||||||
/>
|
/>
|
||||||
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { ClassNames } from '@emotion/core';
|
import { ClassNames } from '@emotion/core';
|
||||||
import { Map, List } from 'immutable';
|
import { Map, List } from 'immutable';
|
||||||
import { ObjectWidgetTopBar, lengths } from 'netlify-cms-ui-default';
|
import { ObjectWidgetTopBar, lengths, colors } from 'netlify-cms-ui-default';
|
||||||
|
|
||||||
const styleStrings = {
|
const styleStrings = {
|
||||||
nestedObjectControl: `
|
nestedObjectControl: `
|
||||||
@ -36,6 +36,7 @@ export default class ObjectControl extends React.Component {
|
|||||||
resolveWidget: PropTypes.func.isRequired,
|
resolveWidget: PropTypes.func.isRequired,
|
||||||
clearFieldErrors: PropTypes.func.isRequired,
|
clearFieldErrors: PropTypes.func.isRequired,
|
||||||
fieldsErrors: ImmutablePropTypes.map.isRequired,
|
fieldsErrors: ImmutablePropTypes.map.isRequired,
|
||||||
|
hasError: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -79,6 +80,7 @@ export default class ObjectControl extends React.Component {
|
|||||||
fieldsErrors,
|
fieldsErrors,
|
||||||
editorControl: EditorControl,
|
editorControl: EditorControl,
|
||||||
controlRef,
|
controlRef,
|
||||||
|
parentIds,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (field.get('widget') === 'hidden') {
|
if (field.get('widget') === 'hidden') {
|
||||||
@ -99,6 +101,7 @@ export default class ObjectControl extends React.Component {
|
|||||||
onValidate={onValidateObject}
|
onValidate={onValidateObject}
|
||||||
processControlRef={controlRef && controlRef.bind(this)}
|
processControlRef={controlRef && controlRef.bind(this)}
|
||||||
controlRef={controlRef}
|
controlRef={controlRef}
|
||||||
|
parentIds={parentIds}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -115,7 +118,7 @@ export default class ObjectControl extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { field, forID, classNameWrapper, forList } = this.props;
|
const { field, forID, classNameWrapper, forList, hasError } = this.props;
|
||||||
const collapsed = forList ? this.props.collapsed : this.state.collapsed;
|
const collapsed = forList ? this.props.collapsed : this.state.collapsed;
|
||||||
const multiFields = field.get('fields');
|
const multiFields = field.get('fields');
|
||||||
const singleField = field.get('field');
|
const singleField = field.get('field');
|
||||||
@ -136,6 +139,11 @@ export default class ObjectControl extends React.Component {
|
|||||||
${styleStrings.nestedObjectControl}
|
${styleStrings.nestedObjectControl}
|
||||||
`]: forList,
|
`]: forList,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
[css`
|
||||||
|
border-color: ${colors.textFieldBorder};
|
||||||
|
`]: forList ? !hasError : false,
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{forList ? null : (
|
{forList ? null : (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user