diff --git a/superset-frontend/spec/javascripts/explore/controlUtils_spec.jsx b/superset-frontend/spec/javascripts/explore/controlUtils_spec.jsx index 946517d89f9b..41b7241adf46 100644 --- a/superset-frontend/spec/javascripts/explore/controlUtils_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/controlUtils_spec.jsx @@ -27,6 +27,7 @@ import { getFormDataFromControls, applyMapStateToPropsToControl, getAllControlsState, + getControlsState, } from 'src/explore/controlUtils'; describe('controlUtils', () => { @@ -248,6 +249,14 @@ describe('controlUtils', () => { const control = getControlState('metrics', 'table', stateWithCount); expect(control.default).toEqual(['count']); }); + + it('should not apply mapStateToProps when initializing', () => { + const control = getControlState('metrics', 'table', { + ...state, + isInitializing: true, + }); + expect(control.default).toEqual(null); + }); }); describe('validateControl', () => { diff --git a/superset-frontend/spec/javascripts/sqllab/ExploreResultsButton_spec.jsx b/superset-frontend/spec/javascripts/sqllab/ExploreResultsButton_spec.jsx index 82bf912bc56d..95511ea6e16e 100644 --- a/superset-frontend/spec/javascripts/sqllab/ExploreResultsButton_spec.jsx +++ b/superset-frontend/spec/javascripts/sqllab/ExploreResultsButton_spec.jsx @@ -201,7 +201,6 @@ describe('ExploreResultsButton', () => { const calls = fetchMock.calls(visualizeEndpoint); expect(calls).toHaveLength(1); const formData = calls[0][1].body; - Object.keys(mockOptions).forEach(key => { // eslint-disable-next-line no-unused-expressions expect(formData.get(key)).toBeDefined(); diff --git a/superset-frontend/src/explore/controlUtils.js b/superset-frontend/src/explore/controlUtils.js index 41c5de08e1dd..1219ff78dd6b 100644 --- a/superset-frontend/src/explore/controlUtils.js +++ b/superset-frontend/src/explore/controlUtils.js @@ -88,15 +88,15 @@ export const getControlConfig = memoizeOne(function getControlConfig( return control?.config || control; }); -export function applyMapStateToPropsToControl(control, state) { - if (control.mapStateToProps) { - const appliedControl = { ...control }; - if (state) { - Object.assign(appliedControl, control.mapStateToProps(state, control)); - } - return appliedControl; +export function applyMapStateToPropsToControl(controlState, controlPanelState) { + const { mapStateToProps } = controlState; + if (mapStateToProps && controlPanelState) { + return { + ...controlState, + ...mapStateToProps(controlPanelState, controlState), + }; } - return control; + return controlState; } function handleMissingChoice(control) { @@ -121,19 +121,37 @@ function handleMissingChoice(control) { return control; } -export function getControlStateFromControlConfig(controlConfig, state, value) { +export function getControlStateFromControlConfig( + controlConfig, + controlPanelState, + value, +) { // skip invalid config values if (!controlConfig) { return null; } - const controlState = applyMapStateToPropsToControl( - { ...controlConfig }, - state, - ); + let controlState = { ...controlConfig }; + // only apply mapStateToProps when control states have been initialized + if ( + controlPanelState && + (controlPanelState.controls || !controlPanelState.isInitializing) + ) { + controlState = applyMapStateToPropsToControl( + controlState, + controlPanelState, + ); + } // If default is a function, evaluate it if (typeof controlState.default === 'function') { - controlState.default = controlState.default(controlState); + controlState.default = controlState.default( + controlState, + controlPanelState, + ); + // if default is still a function, discard + if (typeof controlState.default === 'function') { + delete controlState.default; + } } // If a choice control went from multi=false to true, wrap value in array diff --git a/superset-frontend/src/explore/controls.jsx b/superset-frontend/src/explore/controls.jsx index fc72ed75606c..8b09c9245103 100644 --- a/superset-frontend/src/explore/controls.jsx +++ b/superset-frontend/src/explore/controls.jsx @@ -316,7 +316,6 @@ export const controls = { 'filter below is applied against this column or ' + 'expression', ), - default: control => control.default, clearable: false, optionRenderer: c => , valueRenderer: c => , diff --git a/superset-frontend/src/explore/reducers/getInitialState.js b/superset-frontend/src/explore/reducers/getInitialState.js index 270ef2644c1b..4efa18dcd431 100644 --- a/superset-frontend/src/explore/reducers/getInitialState.js +++ b/superset-frontend/src/explore/reducers/getInitialState.js @@ -21,27 +21,41 @@ import shortid from 'shortid'; import getToastsFromPyFlashMessages from '../../messageToasts/utils/getToastsFromPyFlashMessages'; import { getChartKey } from '../exploreUtils'; import { getControlsState } from '../store'; -import { getFormDataFromControls } from '../controlUtils'; +import { + getFormDataFromControls, + applyMapStateToPropsToControl, +} from '../controlUtils'; export default function getInitialState(bootstrapData) { - const controls = getControlsState(bootstrapData, bootstrapData.form_data); - const rawFormData = { ...bootstrapData.form_data }; - + const { form_data: rawFormData } = bootstrapData; + const slice = bootstrapData.slice; + const sliceName = slice ? slice.slice_name : null; const bootstrappedState = { ...bootstrapData, + sliceName, common: { flash_messages: bootstrapData.common.flash_messages, conf: bootstrapData.common.conf, }, rawFormData, - controls, filterColumnOpts: [], isDatasourceMetaLoading: false, isStarred: false, + isInitializing: true, }; + const controls = getControlsState(bootstrappedState, rawFormData); + bootstrappedState.controls = controls; - const slice = bootstrappedState.slice; - const sliceName = slice ? slice.slice_name : null; + // apply initial mapStateToProps for all controls, must execute AFTER + // bootstrappedState has initialized `controls`. Order of execution is not + // guaranteed, so controls shouldn't rely on the each other's mapped state. + Object.entries(controls).forEach(([key, controlState]) => { + controls[key] = applyMapStateToPropsToControl( + controlState, + bootstrappedState, + ); + }); + bootstrappedState.isInitializing = false; const sliceFormData = slice ? getFormDataFromControls(getControlsState(bootstrapData, slice.form_data)) @@ -69,10 +83,7 @@ export default function getInitialState(bootstrapData) { dashboards: [], saveModalAlert: null, }, - explore: { - ...bootstrappedState, - sliceName, - }, + explore: bootstrappedState, impressionId: shortid.generate(), messageToasts: getToastsFromPyFlashMessages( (bootstrapData.common || {}).flash_messages || [], diff --git a/superset-frontend/src/explore/store.js b/superset-frontend/src/explore/store.js index 1e54901f499a..db456f71213c 100644 --- a/superset-frontend/src/explore/store.js +++ b/superset-frontend/src/explore/store.js @@ -39,7 +39,6 @@ export function getControlsState(state, inputFormData) { * adds value keys coming from inputFormData passed here. This can't be an action creator * just yet because it's used in both the explore and dashboard views. * */ - // Getting a list of active control names for the current viz const formData = { ...inputFormData }; const vizType = formData.viz_type || 'table';