fix: chart validation error not cleared on control value update (#10224)

This commit is contained in:
Jesse Yang 2020-07-01 18:32:27 -07:00 committed by GitHub
parent f7a024d7de
commit 700429f431
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 362 additions and 295 deletions

View File

@ -0,0 +1,72 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
describe('AdhocFilters', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Set simple adhoc filter', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=adhoc_filters]').within(() => {
cy.get('.Select__control').click();
cy.get('input[type=text]').type('name{enter}');
});
cy.get('#filter-edit-popover').within(() => {
cy.get('[data-test=adhoc-filter-simple-value]').within(() => {
cy.get('.Select__control').click();
cy.get('input[type=text]').type('Any{enter}');
});
cy.get('button').contains('Save').click();
});
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@postJson',
chartSelector: 'svg',
});
});
it('Set custom adhoc filter', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=adhoc_filters]').within(() => {
cy.get('.Select__control').click();
cy.get('input[type=text]').type('name{enter}');
});
cy.get('#filter-edit-popover').within(() => {
cy.get('#adhoc-filter-edit-tabs-tab-SQL').click();
cy.get('.ace_content').click();
cy.get('.ace_text-input').type("'Amy' OR name = 'Bob'");
cy.get('button').contains('Save').click();
});
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@postJson',
chartSelector: 'svg',
});
});
});

View File

@ -0,0 +1,131 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
describe('AdhocMetrics', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Clear metric and set simple adhoc metric', () => {
const metric = 'sum(sum_girls)';
const metricName = 'Girl Births';
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=metrics]').within(() => {
cy.get('.Select__clear-indicator').click();
cy.get('.Select__control input').type('sum_girls');
cy.get('.Select__option--is-focused').trigger('mousedown').click();
});
cy.get('#metrics-edit-popover').within(() => {
cy.get('.popover-title').within(() => {
cy.get('span').click();
cy.get('input').type(metricName);
});
cy.get('button').contains('Save').click();
});
cy.get('.Select__multi-value__label').contains(metricName);
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@postJson',
querySubstring: `${metric} AS "${metricName}"`, // SQL statement
chartSelector: 'svg',
});
});
it('Switch from simple to custom sql', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' });
// select column "num"
cy.get('[data-test=metrics]').within(() => {
cy.get('.Select__clear-indicator').click();
cy.get('.Select__control').click();
cy.get('.Select__control input').type('num');
cy.get('.option-label').contains(/^num$/).click();
});
// add custom SQL
cy.get('#metrics-edit-popover').within(() => {
cy.get('#adhoc-metric-edit-tabs-tab-SQL').click();
cy.get('.ace_content').click();
cy.get('.ace_text-input').type('/COUNT(DISTINCT name)', { force: true });
cy.get('button').contains('Save').click();
});
cy.get('button.query').click();
const metric = 'SUM(num)/COUNT(DISTINCT name)';
cy.verifySliceSuccess({
waitAlias: '@postJson',
querySubstring: `${metric} AS "${metric}"`,
chartSelector: 'svg',
});
});
it('Switch from custom sql tabs to simple', () => {
cy.get('[data-test=metrics]').within(() => {
cy.get('.Select__dropdown-indicator').click();
cy.get('input[type=text]').type('sum_girls{enter}');
});
cy.get('#metrics-edit-popover').within(() => {
cy.get('#adhoc-metric-edit-tabs-tab-SQL').click();
cy.get('.ace_identifier').contains('sum_girls');
cy.get('.ace_content').click();
cy.get('.ace_text-input').type('{selectall}{backspace}SUM(num)');
cy.get('#adhoc-metric-edit-tabs-tab-SIMPLE').click();
cy.get('.Select__single-value').contains(/^num$/);
cy.get('button').contains('Save').click();
});
cy.get('button.query').click();
const metric = 'SUM(num)';
cy.verifySliceSuccess({
waitAlias: '@postJson',
querySubstring: `${metric} AS "${metric}"`,
chartSelector: 'svg',
});
});
it('Typing starts with aggregate function name', () => {
// select column "num"
cy.get('[data-test=metrics]').within(() => {
cy.get('.Select__dropdown-indicator').click();
cy.get('.Select__control input[type=text]').type('avg(');
cy.get('.Select__option').contains('ds');
cy.get('.Select__option').contains('name');
cy.get('.Select__option').contains('sum_boys').click();
});
const metric = 'AVG(sum_boys)';
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@postJson',
querySubstring: `${metric} AS "${metric}"`,
chartSelector: 'svg',
});
});
});

View File

@ -0,0 +1,97 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
describe('Advanced analytics', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Create custom time compare', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('.panel-title').contains('Advanced Analytics').click();
cy.get('[data-test=time_compare]').within(() => {
cy.get('.Select__control').click();
cy.get('input[type=text]').type('28 days{enter}');
cy.get('.Select__control').click();
cy.get('input[type=text]').type('364 days{enter}');
cy.get('.Select__multi-value__label').contains('364 days');
});
cy.get('button.query').click();
cy.wait('@postJson');
cy.reload();
cy.verifySliceSuccess({
waitAlias: '@postJson',
chartSelector: 'svg',
});
cy.get('[data-test=time_compare]').within(() => {
cy.get('.Select__multi-value__label').contains('364 days');
cy.get('.Select__multi-value__label').contains('28 days');
});
});
});
describe('Annotations', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Create formula annotation y-axis goal line', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=annotation_layers]').within(() => {
cy.get('button').click();
});
cy.get('.popover-content').within(() => {
cy.get('[data-test=annotation-layer-name-header]')
.siblings()
.first()
.within(() => {
cy.get('input').type('Goal line');
});
cy.get('[data-test=annotation-layer-value-header]')
.siblings()
.first()
.within(() => {
cy.get('input').type('y=1400000');
});
cy.get('button').contains('OK').click();
});
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@postJson',
chartSelector: 'svg',
});
cy.get('.nv-legend-text').should('have.length', 2);
});
});

View File

@ -40,255 +40,6 @@ describe('Groupby', () => {
});
});
describe('AdhocMetrics', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Clear metric and set simple adhoc metric', () => {
const metric = 'sum(sum_girls)';
const metricName = 'Girl Births';
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=metrics]').within(() => {
cy.get('.Select__clear-indicator').click();
cy.get('.Select__control input').type('sum_girls');
cy.get('.Select__option--is-focused').trigger('mousedown').click();
});
cy.get('#metrics-edit-popover').within(() => {
cy.get('.popover-title').within(() => {
cy.get('span').click();
cy.get('input').type(metricName);
});
cy.get('button').contains('Save').click();
});
cy.get('.Select__multi-value__label').contains(metricName);
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@postJson',
querySubstring: `${metric} AS "${metricName}"`, // SQL statement
chartSelector: 'svg',
});
});
it('Switch from simple to custom sql', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' });
// select column "num"
cy.get('[data-test=metrics]').within(() => {
cy.get('.Select__clear-indicator').click();
cy.get('.Select__control').click();
cy.get('.Select__control input').type('num');
cy.get('.option-label').contains(/^num$/).click();
});
// add custom SQL
cy.get('#metrics-edit-popover').within(() => {
cy.get('#adhoc-metric-edit-tabs-tab-SQL').click();
cy.get('.ace_content').click();
cy.get('.ace_text-input').type('/COUNT(DISTINCT name)', { force: true });
cy.get('button').contains('Save').click();
});
cy.get('button.query').click();
const metric = 'SUM(num)/COUNT(DISTINCT name)';
cy.verifySliceSuccess({
waitAlias: '@postJson',
querySubstring: `${metric} AS "${metric}"`,
chartSelector: 'svg',
});
});
it('Switch from custom sql tabs to simple', () => {
cy.get('[data-test=metrics]').within(() => {
cy.get('.Select__dropdown-indicator').click();
cy.get('input[type=text]').type('sum_girls{enter}');
});
cy.get('#metrics-edit-popover').within(() => {
cy.get('#adhoc-metric-edit-tabs-tab-SQL').click();
cy.get('.ace_identifier').contains('sum_girls');
cy.get('.ace_content').click();
cy.get('.ace_text-input').type('{selectall}{backspace}SUM(num)');
cy.get('#adhoc-metric-edit-tabs-tab-SIMPLE').click();
cy.get('.Select__single-value').contains(/^num$/);
cy.get('button').contains('Save').click();
});
cy.get('button.query').click();
const metric = 'SUM(num)';
cy.verifySliceSuccess({
waitAlias: '@postJson',
querySubstring: `${metric} AS "${metric}"`,
chartSelector: 'svg',
});
});
it('Typing starts with aggregate function name', () => {
// select column "num"
cy.get('[data-test=metrics]').within(() => {
cy.get('.Select__dropdown-indicator').click();
cy.get('.Select__control input[type=text]').type('avg(');
cy.get('.Select__option').contains('ds');
cy.get('.Select__option').contains('name');
cy.get('.Select__option').contains('sum_boys').click();
});
const metric = 'AVG(sum_boys)';
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@postJson',
querySubstring: `${metric} AS "${metric}"`,
chartSelector: 'svg',
});
});
});
describe('AdhocFilters', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Set simple adhoc filter', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=adhoc_filters]').within(() => {
cy.get('.Select__control').click();
cy.get('input[type=text]').type('name{enter}');
});
cy.get('#filter-edit-popover').within(() => {
cy.get('[data-test=adhoc-filter-simple-value]').within(() => {
cy.get('.Select__control').click();
cy.get('input[type=text]').type('Any{enter}');
});
cy.get('button').contains('Save').click();
});
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@postJson',
chartSelector: 'svg',
});
});
it('Set custom adhoc filter', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=adhoc_filters]').within(() => {
cy.get('.Select__control').click();
cy.get('input[type=text]').type('name{enter}');
});
cy.get('#filter-edit-popover').within(() => {
cy.get('#adhoc-filter-edit-tabs-tab-SQL').click();
cy.get('.ace_content').click();
cy.get('.ace_text-input').type("'Amy' OR name = 'Bob'");
cy.get('button').contains('Save').click();
});
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@postJson',
chartSelector: 'svg',
});
});
});
describe('Advanced analytics', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Create custom time compare', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('.panel-title').contains('Advanced Analytics').click();
cy.get('[data-test=time_compare]').within(() => {
cy.get('.Select__control').click();
cy.get('input[type=text]').type('28 days{enter}');
cy.get('.Select__control').click();
cy.get('input[type=text]').type('364 days{enter}');
cy.get('.Select__multi-value__label').contains('364 days');
});
cy.get('button.query').click();
cy.wait('@postJson');
cy.reload();
cy.verifySliceSuccess({
waitAlias: '@postJson',
chartSelector: 'svg',
});
cy.get('[data-test=time_compare]').within(() => {
cy.get('.Select__multi-value__label').contains('364 days');
cy.get('.Select__multi-value__label').contains('28 days');
});
});
});
describe('Annotations', () => {
beforeEach(() => {
cy.login();
cy.server();
cy.route('GET', '/superset/explore_json/**').as('getJson');
cy.route('POST', '/superset/explore_json/**').as('postJson');
});
it('Create formula annotation y-axis goal line', () => {
cy.visitChartByName('Num Births Trend');
cy.verifySliceSuccess({ waitAlias: '@postJson' });
cy.get('[data-test=annotation_layers]').within(() => {
cy.get('button').click();
});
cy.get('.popover-content').within(() => {
cy.get('[data-test=annotation-layer-name-header]')
.siblings()
.first()
.within(() => {
cy.get('input').type('Goal line');
});
cy.get('[data-test=annotation-layer-value-header]')
.siblings()
.first()
.within(() => {
cy.get('input').type('y=1400000');
});
cy.get('button').contains('OK').click();
});
cy.get('button.query').click();
cy.verifySliceSuccess({
waitAlias: '@postJson',
chartSelector: 'svg',
});
cy.get('.nv-legend-text').should('have.length', 2);
});
});
describe('Time range filter', () => {
beforeEach(() => {
cy.login();

View File

@ -27,9 +27,26 @@ describe('Visualization > Line', () => {
cy.route('POST', '/superset/explore_json/**').as('getJson');
});
it('should show validator error when no metric', () => {
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
cy.visitChartByParams(JSON.stringify(formData));
cy.get('.alert-warning').contains(`"Metrics" cannot be empty`);
});
it('should not show validator error when metric added', () => {
const formData = { ...LINE_CHART_DEFAULTS, metrics: [] };
cy.visitChartByParams(JSON.stringify(formData));
cy.get('.alert-warning').contains(`"Metrics" cannot be empty`);
cy.get('.text-danger').contains('Metrics');
cy.get('.metrics-select .Select__input input:eq(0)')
.focus()
.type('SUM(num){enter}');
cy.get('.text-danger').should('not.exist');
cy.get('.alert-warning').should('not.exist');
});
it('should work with adhoc metric', () => {
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
cy.visitChartByParams(JSON.stringify(formData));
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
});

View File

@ -30,9 +30,7 @@ describe('Visualization > Table', () => {
});
it('Test table with adhoc metric', () => {
const formData = { ...VIZ_DEFAULTS, metrics: NUM_METRIC };
cy.visitChartByParams(JSON.stringify(formData));
cy.visitChartByParams({ ...VIZ_DEFAULTS, metrics: NUM_METRIC });
cy.verifySliceSuccess({
waitAlias: '@getJson',
querySubstring: NUM_METRIC.label,
@ -41,16 +39,14 @@ describe('Visualization > Table', () => {
});
it('Test table with groupby', () => {
const formData = {
cy.visitChartByParams({
...VIZ_DEFAULTS,
metrics: NUM_METRIC,
groupby: ['name'],
};
cy.visitChartByParams(JSON.stringify(formData));
});
cy.verifySliceSuccess({
waitAlias: '@getJson',
querySubstring: formData.groupby[0],
querySubstring: /groupby.*name/,
chartSelector: 'table',
});
});
@ -62,7 +58,6 @@ describe('Visualization > Table', () => {
metrics: [],
groupby: ['name'],
};
cy.visitChartByParams(JSON.stringify(formData));
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'table' });
});
@ -74,23 +69,19 @@ describe('Visualization > Table', () => {
groupby: ['name'],
order_desc: true,
};
cy.visitChartByParams(JSON.stringify(formData));
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'table' });
});
it('Test table with groupby and limit', () => {
const limit = 10;
const formData = {
...VIZ_DEFAULTS,
metrics: NUM_METRIC,
groupby: ['name'],
row_limit: limit,
};
cy.visitChartByParams(JSON.stringify(formData));
cy.wait('@getJson').then(async xhr => {
cy.verifyResponseCodes(xhr);
cy.verifySliceContainer('table');

View File

@ -30,6 +30,10 @@ declare namespace Cypress {
*/
login(): void;
visitChartByParams(params: string | object): cy;
visitChartByName(name: string): cy;
visitChartById(id: number): cy;
/**
* Verify a waitXHR response and parse response JSON.
*/
@ -46,14 +50,10 @@ declare namespace Cypress {
/**
* Verify slice successfully loaded.
*/
verifySliceSuccess({
waitAlias,
querySubString,
chartSelector,
}: {
verifySliceSuccess(options: {
waitAlias: string;
querySubString: string;
chartSelector: JQuery.Selector;
querySubstring?: string | RegExp;
chartSelector?: JQuery.Selector;
}): cy;
}
}

View File

@ -42,7 +42,9 @@ Cypress.Commands.add('visitChartById', chartId => {
});
Cypress.Commands.add('visitChartByParams', params => {
return cy.visit(`${BASE_EXPLORE_URL}${params}`);
const queryString =
typeof params === 'string' ? params : JSON.stringify(params);
return cy.visit(`${BASE_EXPLORE_URL}${queryString}`);
});
Cypress.Commands.add('verifyResponseCodes', (xhr: XMLHttpRequest, callback) => {
@ -78,17 +80,21 @@ Cypress.Commands.add(
chartSelector,
}: {
waitAlias: string;
querySubstring: string;
chartSelector: JQuery.Selector;
querySubstring?: string | RegExp;
}) => {
cy.wait(waitAlias).then(xhr => {
cy.verifySliceContainer(chartSelector);
cy.verifyResponseCodes(xhr, responseBody => {
if (querySubstring) {
type QueryResponse = { query: string };
expect(
responseBody && (responseBody as QueryResponse).query,
).contains(querySubstring);
const query = responseBody
? (responseBody as { query: string }).query
: '';
if (querySubstring instanceof RegExp) {
expect(query).to.match(querySubstring);
} else {
expect(query).to.contain(querySubstring);
}
}
});
});

View File

@ -189,15 +189,15 @@ describe('controlUtils', () => {
it('removes the mapStateToProps key from the object', () => {
let control = getControlConfig('all_columns', 'table');
control = applyMapStateToPropsToControl(control, state);
expect(control.mapStateToProps).toBe(undefined);
expect(control.mapStateToProps[0]).toBe(undefined);
});
});
describe('getControlState', () => {
it('to be function free', () => {
const control = getControlState('all_columns', 'table', state, ['a']);
expect(control.mapStateToProps).toBe(undefined);
expect(control.validators).toBe(undefined);
it('to still have the functions', () => {
const control = getControlState('metrics', 'table', state, ['a']);
expect(typeof control.mapStateToProps).toBe('function');
expect(typeof control.validators[0]).toBe('function');
});
it('to fix multi with non-array values', () => {

View File

@ -76,13 +76,6 @@ class ControlPanelsContainer extends React.Component {
// apply current value in formData
value: formData[name],
};
const { mapStateToProps: mapFn } = controlData;
if (mapFn) {
Object.assign(
controlData,
mapFn(exploreState, controlData, actions) || {},
);
}
const {
validationErrors,
provideFormDataToProps,

View File

@ -45,7 +45,6 @@ export function validateControl(control, processedState) {
validationErrors.push(v);
}
});
delete validatedControl.validators;
return { ...validatedControl, validationErrors };
}
return control;
@ -95,7 +94,6 @@ export function applyMapStateToPropsToControl(control, state) {
if (state) {
Object.assign(appliedControl, control.mapStateToProps(state, control));
}
delete appliedControl.mapStateToProps;
return appliedControl;
}
return control;
@ -141,6 +139,7 @@ export function getControlStateFromControlConfig(controlConfig, state, value) {
// If a choice control went from multi=false to true, wrap value in array
const controlValue =
controlConfig.multi && value && !Array.isArray(value) ? [value] : value;
controlState.value =
typeof controlValue === 'undefined' ? controlState.default : controlValue;

View File

@ -99,24 +99,34 @@ export default function exploreReducer(state = {}, action) {
},
[actions.SET_FIELD_VALUE]() {
const new_form_data = state.form_data;
new_form_data[action.controlName] = action.value;
const { controlName, value, validationErrors } = action;
new_form_data[controlName] = value;
// These errors are reported from the Control components
let errors = action.validationErrors || [];
const vizType = new_form_data.viz_type;
// Use the processed control config (with overrides and everything)
// if `controlName` does not existing in current controls,
const controlConfig =
state.controls[action.controlName] ||
getControlConfig(action.controlName, vizType) ||
{};
// will call validators again
const control = {
...getControlStateFromControlConfig(controlConfig, state, action.value),
};
// These errors are based on control config `validators`
errors = errors.concat(control.validationErrors || []);
// combine newly detected errors with errors from `onChange` event of
// each control component (passed via reducer action).
const errors = control.validationErrors || [];
(validationErrors || []).forEach(err => {
// skip duplicated errors
if (!errors.includes(err)) {
errors.push(err);
}
});
const hasErrors = errors && errors.length > 0;
return {
...state,
form_data: new_form_data,