This is an automated email from the ASF dual-hosted git repository.

yjc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 700429f  fix: chart validation error not cleared on control value 
update (#10224)
700429f is described below

commit 700429f431d20b624e886cf554b2b493793448be
Author: Jesse Yang <jesse.y...@airbnb.com>
AuthorDate: Wed Jul 1 18:32:27 2020 -0700

    fix: chart validation error not cleared on control value update (#10224)
---
 .../integration/explore/AdhocFilters.test.ts       |  72 ++++++
 .../integration/explore/AdhocMetrics.test.ts       | 131 +++++++++++
 .../cypress/integration/explore/advanced.test.ts   |  97 ++++++++
 .../cypress/integration/explore/control.test.js    | 249 ---------------------
 .../explore/visualizations/line.test.js            |  19 +-
 .../{table.test.js => table.test.ts}               |  17 +-
 .../cypress-base/cypress/support/index.d.ts        |  14 +-
 .../cypress-base/cypress/support/index.ts          |  18 +-
 .../spec/javascripts/explore/controlUtils_spec.jsx |  10 +-
 .../explore/components/ControlPanelsContainer.jsx  |   7 -
 superset-frontend/src/explore/controlUtils.js      |   3 +-
 .../src/explore/reducers/exploreReducer.js         |  20 +-
 12 files changed, 362 insertions(+), 295 deletions(-)

diff --git 
a/superset-frontend/cypress-base/cypress/integration/explore/AdhocFilters.test.ts
 
b/superset-frontend/cypress-base/cypress/integration/explore/AdhocFilters.test.ts
new file mode 100644
index 0000000..dc12961
--- /dev/null
+++ 
b/superset-frontend/cypress-base/cypress/integration/explore/AdhocFilters.test.ts
@@ -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',
+    });
+  });
+});
diff --git 
a/superset-frontend/cypress-base/cypress/integration/explore/AdhocMetrics.test.ts
 
b/superset-frontend/cypress-base/cypress/integration/explore/AdhocMetrics.test.ts
new file mode 100644
index 0000000..ef7dc9b
--- /dev/null
+++ 
b/superset-frontend/cypress-base/cypress/integration/explore/AdhocMetrics.test.ts
@@ -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',
+    });
+  });
+});
diff --git 
a/superset-frontend/cypress-base/cypress/integration/explore/advanced.test.ts 
b/superset-frontend/cypress-base/cypress/integration/explore/advanced.test.ts
new file mode 100644
index 0000000..4a21f00
--- /dev/null
+++ 
b/superset-frontend/cypress-base/cypress/integration/explore/advanced.test.ts
@@ -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);
+  });
+});
diff --git 
a/superset-frontend/cypress-base/cypress/integration/explore/control.test.js 
b/superset-frontend/cypress-base/cypress/integration/explore/control.test.js
index b012a13..e2da3a6 100644
--- a/superset-frontend/cypress-base/cypress/integration/explore/control.test.js
+++ b/superset-frontend/cypress-base/cypress/integration/explore/control.test.js
@@ -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();
diff --git 
a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.js
 
b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.js
index 5314a01..229c7f4 100644
--- 
a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.js
+++ 
b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.js
@@ -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' });
   });
diff --git 
a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.js
 
b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts
similarity index 95%
rename from 
superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.js
rename to 
superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts
index e95fb48..b7db68e 100644
--- 
a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.js
+++ 
b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts
@@ -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');
diff --git a/superset-frontend/cypress-base/cypress/support/index.d.ts 
b/superset-frontend/cypress-base/cypress/support/index.d.ts
index 80a936e..0c21db7 100644
--- a/superset-frontend/cypress-base/cypress/support/index.d.ts
+++ b/superset-frontend/cypress-base/cypress/support/index.d.ts
@@ -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;
   }
 }
diff --git a/superset-frontend/cypress-base/cypress/support/index.ts 
b/superset-frontend/cypress-base/cypress/support/index.ts
index ad70b73..759539c 100644
--- a/superset-frontend/cypress-base/cypress/support/index.ts
+++ b/superset-frontend/cypress-base/cypress/support/index.ts
@@ -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);
+          }
         }
       });
     });
diff --git a/superset-frontend/spec/javascripts/explore/controlUtils_spec.jsx 
b/superset-frontend/spec/javascripts/explore/controlUtils_spec.jsx
index 2b18063..946517d 100644
--- a/superset-frontend/spec/javascripts/explore/controlUtils_spec.jsx
+++ b/superset-frontend/spec/javascripts/explore/controlUtils_spec.jsx
@@ -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', () => {
diff --git 
a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx 
b/superset-frontend/src/explore/components/ControlPanelsContainer.jsx
index 22b948f..77aa5a7 100644
--- a/superset-frontend/src/explore/components/ControlPanelsContainer.jsx
+++ b/superset-frontend/src/explore/components/ControlPanelsContainer.jsx
@@ -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,
diff --git a/superset-frontend/src/explore/controlUtils.js 
b/superset-frontend/src/explore/controlUtils.js
index daa395e..41c5de0 100644
--- a/superset-frontend/src/explore/controlUtils.js
+++ b/superset-frontend/src/explore/controlUtils.js
@@ -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;
 
diff --git a/superset-frontend/src/explore/reducers/exploreReducer.js 
b/superset-frontend/src/explore/reducers/exploreReducer.js
index 5e1df2c..83cd62e 100644
--- a/superset-frontend/src/explore/reducers/exploreReducer.js
+++ b/superset-frontend/src/explore/reducers/exploreReducer.js
@@ -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,

Reply via email to