diff --git a/dev/App.js b/dev/App.js index fd8349776..7ff298846 100644 --- a/dev/App.js +++ b/dev/App.js @@ -14,6 +14,8 @@ import 'brace/theme/textmate'; // https://github.com/plotly/react-chart-editor#mapbox-access-tokens import ACCESS_TOKENS from '../accessTokens'; +import {customConfigTest} from '../src/__stories__'; + const dataSourceOptions = Object.keys(dataSources).map(name => ({ value: name, label: name, @@ -183,6 +185,7 @@ class App extends Component { // makeDefaultTrace={() => ({type: 'scattergl', mode: 'markers'})} // fontOptions={[{label:'Arial', value: 'arial'}]} // chartHelp={chartHelp} + // customConfig={customConfigTest} > {this.props.children} @@ -91,6 +92,7 @@ PlotlyEditor.propTypes = { glByDefault: PropTypes.bool, fontOptions: PropTypes.array, chartHelp: PropTypes.object, + customConfig: PropTypes.object, }; PlotlyEditor.defaultProps = { diff --git a/src/__stories__/index.js b/src/__stories__/index.js index a6586e3de..38afd39aa 100644 --- a/src/__stories__/index.js +++ b/src/__stories__/index.js @@ -12,6 +12,72 @@ import './stories.css'; import React from 'react'; import {storiesOf} from '@storybook/react'; +export const customConfigTest = { + visibility_rules: { + blacklist: [ + {type: 'attrName', regex_match: 'font.family'}, + {type: 'attrName', regex_match: 'font.size'}, + { + type: 'attrName', + regex_match: 'color', + exceptions: [ + { + type: 'attrName', + regex_match: 'colorbar', + exceptions: [ + {type: 'attrName', regex_match: 'colorbar.bgcolor'}, + {type: 'attrName', regex_match: 'colorbar.tickfont.color'}, + {type: 'attrName', regex_match: 'colorbar.title.font.color'}, + {type: 'attrName', regex_match: 'colorbar.outlinecolor'}, + {type: 'attrName', regex_match: 'colorbar.bordercolor'}, + {type: 'attrName', regex_match: 'colorbar.tickcolor'}, + ], + }, + { + type: 'attrName', + regex_match: 'coloraxis', + exceptions: [ + {type: 'attrName', regex_match: 'coloraxis.colorscale'}, + {type: 'attrName', regex_match: 'coloraxis.colorbar.outlinecolor'}, + {type: 'attrName', regex_match: 'coloraxis.colorbar.bordercolor'}, + {type: 'attrName', regex_match: 'coloraxis.colorbar.bgcolor'}, + {type: 'attrName', regex_match: 'coloraxis.colorbar.tickcolor'}, + {type: 'attrName', regex_match: 'coloraxis.colorbar.tickfont.color'}, + {type: 'attrName', regex_match: 'coloraxis.colorbar.title.font.color'}, + ], + }, + { + type: 'attrName', + regex_match: 'colorscales', + exceptions: [ + { + type: 'attrName', + regex_match: 'colorscales.items.concentrationscales.colorscale', + }, + ], + }, + {type: 'attrName', regex_match: 'autocolorscale'}, + {type: 'attrName', regex_match: 'usecolormap'}, + {type: 'attrName', regex_match: 'bundlecolors'}, + { + type: 'attrName', + regex_match: 'marker.color', + exceptions: [ + {type: 'controlType', regex_match: '^UnconnectedMultiColorPicker$'}, + {type: 'controlType', regex_match: '^UnconnectedColorscalePicker$'}, + {type: 'attrName', regex_match: 'marker.colorbar.outlinecolor'}, + {type: 'attrName', regex_match: 'marker.colorbar.bordercolor'}, + {type: 'attrName', regex_match: 'marker.colorbar.bgcolor'}, + {type: 'attrName', regex_match: 'marker.colorbar.tickcolor'}, + {type: 'attrName', regex_match: 'marker.colorbar.tickfont.color'}, + {type: 'attrName', regex_match: 'marker.colorbar.title.font.color'}, + ], + }, + ], + }, + ], + }, +}; /** * To add more Percy tests - add a mock file to /dev/percy, add it to /dev/percy/index.js @@ -34,7 +100,7 @@ window.URL.createObjectURL = function() { return null; }; -const panelFixture = (Panel, group, name, figure) => { +const panelFixture = (Panel, group, name, figure, customConfig) => { const gd = setupGraphDiv(figure, plotly); gd._context = plotly.setPlotConfig(); gd._context.setBackground = () => { @@ -48,6 +114,7 @@ const panelFixture = (Panel, group, name, figure) => { graphDiv={gd} dataSources={fixtures.scatter().dataSources} dataSourceOptions={fixtures.scatter().dataSourceOptions} + customConfig={customConfig || {}} > @@ -67,8 +134,10 @@ Object.keys(mocks).forEach(m => { const panelGroup = words[0]; const panelName = words.slice(1, -1).join(' '); - stories = stories.add(`${m}_${p}`, () => - panelFixture(panels[p], panelGroup, panelName, mocks[m]) - ); + stories = stories + .add(`${m}_${p}`, () => panelFixture(panels[p], panelGroup, panelName, mocks[m])) + .add(`${m}_${p}_withCustomConfig`, () => + panelFixture(panels[p], panelGroup, panelName, mocks[m], customConfigTest) + ); }); }); diff --git a/src/components/containers/PlotlySection.js b/src/components/containers/PlotlySection.js index 677be29da..21a615897 100644 --- a/src/components/containers/PlotlySection.js +++ b/src/components/containers/PlotlySection.js @@ -1,6 +1,10 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; -import {containerConnectedContextTypes, unpackPlotProps} from '../../lib'; +import { + containerConnectedContextTypes, + unpackPlotProps, + isVisibleGivenCustomConfig, +} from '../../lib'; export class Section extends Component { constructor() { @@ -45,7 +49,7 @@ export default class PlotlySection extends Section { determineVisibility(nextProps, nextContext) { const {isVisible} = unpackPlotProps(nextProps, nextContext); - this.sectionVisible = Boolean(isVisible); + this.sectionVisible = isVisibleGivenCustomConfig(isVisible, nextProps, nextContext); React.Children.forEach(nextProps.children, child => { if (!child || this.sectionVisible) { @@ -57,7 +61,13 @@ export default class PlotlySection extends Section { if (child.type.modifyPlotProps) { child.type.modifyPlotProps(child.props, nextContext, plotProps); } - this.sectionVisible = this.sectionVisible || plotProps.isVisible; + + this.sectionVisible = isVisibleGivenCustomConfig( + this.sectionVisible || plotProps.isVisible, + child.props, + nextContext, + child.type && child.type.displayName ? child.type.displayName : null + ); return; } diff --git a/src/components/containers/__tests__/ConnectedContainersVisibility-test.js b/src/components/containers/__tests__/ConnectedContainersVisibility-test.js index d306aefb9..2dad23642 100644 --- a/src/components/containers/__tests__/ConnectedContainersVisibility-test.js +++ b/src/components/containers/__tests__/ConnectedContainersVisibility-test.js @@ -74,6 +74,57 @@ describe('Basic PlotlySection rules', () => { it('HIDES Field', () => expect(wrapper.find('input').length).toEqual(0)); }); + describe('PlotlyPanel > PlotlySection > Field-with-no-visible-attr-based-on-customConfig', () => { + const wrapper = mount( + + + + + + + + ); + + it('HIDES Field based on customConfig', () => + expect(wrapper.find('input').length).toEqual(0)); + it('HIDES PlotlySection because no visible children according to custom config', () => + expect(wrapper.find('div.section').length).toEqual(0)); + }); + + describe('PlotlyPanel > PlotlySection > Field-with-no-visible-attr-based-on-customConfig', () => { + const wrapper = mount( + + + + + + + + + ); + + it('HIDES the title.font.color Field based on customConfig', () => + expect(wrapper.find('input').length).toEqual(1)); + it('SHOWS PlotlySection if it has an attr that is accepted by customConfig', () => + expect(wrapper.find('div.section').length).toEqual(1)); + }); + describe('div > PlotlySection > Field-with-visible-attr', () => { const wrapper = mount( diff --git a/src/components/fields/ColorArrayPicker.js b/src/components/fields/ColorArrayPicker.js index 3855e0c26..982d31805 100644 --- a/src/components/fields/ColorArrayPicker.js +++ b/src/components/fields/ColorArrayPicker.js @@ -130,4 +130,6 @@ UnconnectedColorArrayPicker.contextTypes = { updateContainer: PropTypes.func, }; +UnconnectedColorArrayPicker.displayName = 'UnconnectedColorArrayPicker'; + export default connectToContainer(UnconnectedColorArrayPicker); diff --git a/src/components/fields/ColorPicker.js b/src/components/fields/ColorPicker.js index f14303b83..75d333371 100644 --- a/src/components/fields/ColorPicker.js +++ b/src/components/fields/ColorPicker.js @@ -51,4 +51,6 @@ UnconnectedColorPicker.propTypes = { ...Field.propTypes, }; +UnconnectedColorPicker.displayName = 'UnconnectedColorPicker'; + export default connectToContainer(UnconnectedColorPicker); diff --git a/src/components/fields/ColorscalePicker.js b/src/components/fields/ColorscalePicker.js index 35f636d87..be05e2bca 100644 --- a/src/components/fields/ColorscalePicker.js +++ b/src/components/fields/ColorscalePicker.js @@ -65,4 +65,6 @@ UnconnectedColorscalePicker.contextTypes = { onUpdate: PropTypes.func, }; +UnconnectedColorscalePicker.displayName = 'UnconnectedColorscalePicker'; + export default connectToContainer(UnconnectedColorscalePicker); diff --git a/src/components/fields/ColorwayPicker.js b/src/components/fields/ColorwayPicker.js index 3771a1e57..51f12998a 100644 --- a/src/components/fields/ColorwayPicker.js +++ b/src/components/fields/ColorwayPicker.js @@ -25,4 +25,6 @@ UnconnectedColorwayPicker.propTypes = { ...Field.propTypes, }; +UnconnectedColorwayPicker.displayName = 'UnconnectedColorwayPicker'; + export default connectToContainer(UnconnectedColorwayPicker); diff --git a/src/components/fields/DataSelector.js b/src/components/fields/DataSelector.js index 92ac3e6b1..41f594da1 100644 --- a/src/components/fields/DataSelector.js +++ b/src/components/fields/DataSelector.js @@ -156,6 +156,8 @@ UnconnectedDataSelector.contextTypes = { container: PropTypes.object, }; +UnconnectedDataSelector.displayName = 'UnconnectedDataSelector'; + function modifyPlotProps(props, context, plotProps) { if ( attributeIsData(plotProps.attrMeta) && diff --git a/src/components/fields/DateTimePicker.js b/src/components/fields/DateTimePicker.js index 1cddef6f4..842fee028 100644 --- a/src/components/fields/DateTimePicker.js +++ b/src/components/fields/DateTimePicker.js @@ -25,4 +25,6 @@ UnconnectedDateTimePicker.propTypes = { ...Field.propTypes, }; +UnconnectedDateTimePicker.displayName = 'UnconnectedDateTimePicker'; + export default connectToContainer(UnconnectedDateTimePicker); diff --git a/src/components/fields/Dropdown.js b/src/components/fields/Dropdown.js index 1bbdda58f..08a481c22 100644 --- a/src/components/fields/Dropdown.js +++ b/src/components/fields/Dropdown.js @@ -39,4 +39,6 @@ UnconnectedDropdown.propTypes = { ...Field.propTypes, }; +UnconnectedDropdown.displayName = 'UnconnectedDropdown'; + export default connectToContainer(UnconnectedDropdown); diff --git a/src/components/fields/DropdownCustom.js b/src/components/fields/DropdownCustom.js index ecc314cfb..4b1280307 100644 --- a/src/components/fields/DropdownCustom.js +++ b/src/components/fields/DropdownCustom.js @@ -97,4 +97,6 @@ UnconnectedDropdownCustom.contextTypes = { updateContainer: PropTypes.func, }; +UnconnectedDropdownCustom.displayName = 'UnconnectedDropdownCustom'; + export default connectToContainer(UnconnectedDropdownCustom); diff --git a/src/components/fields/Dropzone.js b/src/components/fields/Dropzone.js index cbeae6547..5bbc3028e 100644 --- a/src/components/fields/Dropzone.js +++ b/src/components/fields/Dropzone.js @@ -24,4 +24,6 @@ UnconnectedDropzone.propTypes = { ...Field.propTypes, }; +UnconnectedDropzone.displayName = 'UnconnectedDropzone'; + export default connectToContainer(UnconnectedDropzone); diff --git a/src/components/fields/DualNumeric.js b/src/components/fields/DualNumeric.js index ce406ddc7..84e61ab74 100644 --- a/src/components/fields/DualNumeric.js +++ b/src/components/fields/DualNumeric.js @@ -90,4 +90,6 @@ UnconnectedDualNumericFraction.contextTypes = { fullContainer: PropTypes.object, }; +UnconnectedDualNumericFraction.displayName = 'UnconnectedDualNumericFraction'; + export default connectToContainer(UnconnectedDualNumericFraction); diff --git a/src/components/fields/Flaglist.js b/src/components/fields/Flaglist.js index aca7199b9..231418500 100644 --- a/src/components/fields/Flaglist.js +++ b/src/components/fields/Flaglist.js @@ -25,4 +25,6 @@ UnconnectedFlaglist.propTypes = { ...Field.propTypes, }; +UnconnectedFlaglist.displayName = 'UnconnectedFlaglist'; + export default connectToContainer(UnconnectedFlaglist); diff --git a/src/components/fields/GroupCreator.js b/src/components/fields/GroupCreator.js index e7fb7ff25..c46ff0dd4 100644 --- a/src/components/fields/GroupCreator.js +++ b/src/components/fields/GroupCreator.js @@ -81,4 +81,6 @@ UnconnectedGroupCreator.contextTypes = { fullData: PropTypes.array, }; +UnconnectedGroupCreator.displayName = 'UnconnectedGroupCreator'; + export default connectToContainer(UnconnectedGroupCreator); diff --git a/src/components/fields/HoverLabelNameLength.js b/src/components/fields/HoverLabelNameLength.js index e29e48708..6ee9630f5 100644 --- a/src/components/fields/HoverLabelNameLength.js +++ b/src/components/fields/HoverLabelNameLength.js @@ -81,6 +81,8 @@ UnconnectedHoverLabelNameLength.contextTypes = { localize: PropTypes.func, }; +UnconnectedHoverLabelNameLength.displayName = 'UnconnectedHoverLabelNameLength'; + export default connectToContainer(UnconnectedHoverLabelNameLength, { modifyPlotProps: (props, context, plotProps) => { const {container} = plotProps; diff --git a/src/components/fields/MarkerColor.js b/src/components/fields/MarkerColor.js index ea657e50f..9c80408ca 100644 --- a/src/components/fields/MarkerColor.js +++ b/src/components/fields/MarkerColor.js @@ -226,4 +226,6 @@ UnconnectedMarkerColor.contextTypes = { container: PropTypes.object, }; +UnconnectedMarkerColor.displayName = 'UnconnectedMarkerColor'; + export default connectToContainer(UnconnectedMarkerColor); diff --git a/src/components/fields/MarkerSize.js b/src/components/fields/MarkerSize.js index def878e00..0d5f400aa 100644 --- a/src/components/fields/MarkerSize.js +++ b/src/components/fields/MarkerSize.js @@ -97,4 +97,6 @@ UnconnectedMarkerSize.contextTypes = { updateContainer: PropTypes.func, }; +UnconnectedMarkerSize.displayName = 'UnconnectedMarkerSize'; + export default connectToContainer(UnconnectedMarkerSize); diff --git a/src/components/fields/MultiColorPicker.js b/src/components/fields/MultiColorPicker.js index d5d6657fd..f1e9f1d84 100644 --- a/src/components/fields/MultiColorPicker.js +++ b/src/components/fields/MultiColorPicker.js @@ -142,6 +142,8 @@ UnconnectedMultiColorPicker.contextTypes = { fullData: PropTypes.array, }; +UnconnectedMultiColorPicker.displayName = 'UnconnectedMultiColorPicker'; + export default connectToContainer(UnconnectedMultiColorPicker, { modifyPlotProps(props, context, plotProps) { if (plotProps.isVisible) { diff --git a/src/components/fields/Numeric.js b/src/components/fields/Numeric.js index f3a991298..846e1b044 100644 --- a/src/components/fields/Numeric.js +++ b/src/components/fields/Numeric.js @@ -47,4 +47,6 @@ UnconnectedNumeric.propTypes = { ...Field.propTypes, }; +UnconnectedNumeric.displayName = 'UnconnectedNumeric'; + export default connectToContainer(UnconnectedNumeric); diff --git a/src/components/fields/NumericOrDate.js b/src/components/fields/NumericOrDate.js index 2f72a6c20..7cb401feb 100644 --- a/src/components/fields/NumericOrDate.js +++ b/src/components/fields/NumericOrDate.js @@ -35,4 +35,6 @@ UnconnectedNumericOrDate.propTypes = { ...Field.propTypes, }; +UnconnectedNumericOrDate.displayName = 'UnconnectedNumericOrDate'; + export default connectToContainer(UnconnectedNumericOrDate); diff --git a/src/components/fields/PieColorscalePicker.js b/src/components/fields/PieColorscalePicker.js index 15fdb4e27..8f68a1219 100644 --- a/src/components/fields/PieColorscalePicker.js +++ b/src/components/fields/PieColorscalePicker.js @@ -47,6 +47,8 @@ UnconnectedPieColorscalePicker.contextTypes = { graphDiv: PropTypes.object, }; +UnconnectedPieColorscalePicker.displayName = 'UnconnectedPieColorscalePicker'; + export default connectToContainer(UnconnectedPieColorscalePicker, { modifyPlotProps: (props, context, plotProps) => { if ( diff --git a/src/components/fields/Radio.js b/src/components/fields/Radio.js index ccf63f9c7..7f8f11242 100644 --- a/src/components/fields/Radio.js +++ b/src/components/fields/Radio.js @@ -32,4 +32,6 @@ UnconnectedRadio.defaultProps = { center: true, }; +UnconnectedRadio.displayName = 'UnconnectedRadio'; + export default connectToContainer(UnconnectedRadio); diff --git a/src/components/fields/RectanglePositioner.js b/src/components/fields/RectanglePositioner.js index c14478b34..6e7b1970e 100644 --- a/src/components/fields/RectanglePositioner.js +++ b/src/components/fields/RectanglePositioner.js @@ -185,4 +185,6 @@ UnconnectedRectanglePositioner.contextTypes = { fullLayout: PropTypes.object, }; +UnconnectedRectanglePositioner.displayName = 'UnconnectedRectanglePositioner'; + export default connectToContainer(UnconnectedRectanglePositioner); diff --git a/src/components/fields/Text.js b/src/components/fields/Text.js index 0d888c80d..b9c62cb48 100644 --- a/src/components/fields/Text.js +++ b/src/components/fields/Text.js @@ -36,4 +36,6 @@ UnconnectedText.propTypes = { ...Field.propTypes, }; +UnconnectedText.displayName = 'UnconnectedText'; + export default connectToContainer(UnconnectedText); diff --git a/src/components/fields/TextEditor.js b/src/components/fields/TextEditor.js index c5a0a76f7..676e6e7f3 100644 --- a/src/components/fields/TextEditor.js +++ b/src/components/fields/TextEditor.js @@ -127,6 +127,8 @@ UnconnectedTextEditor.contextTypes = { fullLayout: PropTypes.object, }; +UnconnectedTextEditor.displayName = 'UnconnectedTextEditor'; + export default connectToContainer(UnconnectedTextEditor, { modifyPlotProps: (props, context, plotProps) => { if (plotProps.isVisible && plotProps.multiValued) { diff --git a/src/components/fields/TextPosition.js b/src/components/fields/TextPosition.js index d0a12efa4..ddbffbb20 100644 --- a/src/components/fields/TextPosition.js +++ b/src/components/fields/TextPosition.js @@ -77,6 +77,8 @@ UnconnectedTextPosition.contextTypes = { localize: PropTypes.func, }; +UnconnectedTextPosition.displayName = 'UnconnectedTextPosition'; + export default connectToContainer(UnconnectedTextPosition, { modifyPlotProps: (props, context, plotProps) => { const {localize: _} = context; diff --git a/src/components/fields/VisibilitySelect.js b/src/components/fields/VisibilitySelect.js index d36bb98da..b46da658a 100644 --- a/src/components/fields/VisibilitySelect.js +++ b/src/components/fields/VisibilitySelect.js @@ -83,4 +83,6 @@ UnconnectedVisibilitySelect.contextTypes = { updateContainer: PropTypes.func, }; +UnconnectedVisibilitySelect.displayName = 'UnconnectedVisibilitySelect'; + export default connectToContainer(UnconnectedVisibilitySelect); diff --git a/src/components/fields/derived.js b/src/components/fields/derived.js index 569eda5fc..bc6d8822b 100644 --- a/src/components/fields/derived.js +++ b/src/components/fields/derived.js @@ -254,6 +254,7 @@ UnconnectedNumericFraction.defaultProps = { units: '%', showSlider: true, }; +UnconnectedNumericFraction.displayName = 'UnconnectedNumericFraction'; const numericFractionModifyPlotProps = (props, context, plotProps) => { const {attrMeta, fullValue, updatePlot} = plotProps; diff --git a/src/lib/__tests__/unpackPlotProps-test.js b/src/lib/__tests__/unpackPlotProps-test.js new file mode 100644 index 000000000..fe443a93f --- /dev/null +++ b/src/lib/__tests__/unpackPlotProps-test.js @@ -0,0 +1,88 @@ +import {computeCustomConfigVisibility} from '../index'; + +const validate = (string, expected, config, wrappedComponentDisplayName) => { + const isVisible = computeCustomConfigVisibility( + {attr: string}, + config, + wrappedComponentDisplayName + ); + expect(isVisible).toBe(expected[string]); +}; + +describe('computeCustomConfigVisibility', () => { + const customConfig = { + visibility_rules: { + blacklist: [ + { + type: 'attrName', + regex_match: 'color', + exceptions: [ + { + type: 'attrName', + regex_match: 'colorscale', + exceptions: [ + {type: 'attrName', regex_match: 'colorscale.title.font.color'}, + {type: 'attrName', regex_match: 'colorscale.tickcolor'}, + ], + }, + ], + }, + ], + }, + }; + + it('correctly blacklists attributes taking into account exceptions', () => { + const cases = { + bg_color: false, + 'font.color': false, + somethingElse: true, + colorscale: true, + 'colorscale.somethingElse': true, + 'colorscale.title.font.color': false, + 'colorscale.tickcolor': false, + }; + + Object.keys(cases).forEach(c => validate(c, cases, customConfig)); + }); + + it('correctly whitelists attributes taking into account exceptions', () => { + const config = {visibility_rules: {whitelist: customConfig.visibility_rules.blacklist}}; + + const cases = { + bg_color: true, + 'font.color': true, + somethingElse: false, + colorscale: false, + 'colorscale.somethingElse': false, + 'colorscale.title.font.color': true, + 'colorscale.tickcolor': true, + }; + + Object.keys(cases).forEach(c => validate(c, cases, config)); + }); + + it('correctly displays visibility based on controlType', () => { + const config = { + visibility_rules: { + blacklist: [ + { + type: 'attrName', + regex_match: 'color', + exceptions: [ + { + type: 'attrName', + regex_match: 'marker.color', + exceptions: [{type: 'controlType', regex_match: '^ColorscalePicker$'}], + }, + ], + }, + ], + }, + }; + + const case1 = {'marker.color': false}; + const case2 = {'marker.color': true}; + Object.keys(case1).forEach(c => validate(c, case1, config, 'ColorscalePicker')); + Object.keys(case2).forEach(c => validate(c, case2, config, 'AnotherPicker')); + }); +}); diff --git a/src/lib/connectToContainer.js b/src/lib/connectToContainer.js index 0a5fedee5..62e566e87 100644 --- a/src/lib/connectToContainer.js +++ b/src/lib/connectToContainer.js @@ -1,6 +1,6 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; -import unpackPlotProps from './unpackPlotProps'; +import unpackPlotProps, {isVisibleGivenCustomConfig} from './unpackPlotProps'; import {getDisplayName} from '../lib'; export const containerConnectedContextTypes = { @@ -18,6 +18,8 @@ export const containerConnectedContextTypes = { plotly: PropTypes.object, updateContainer: PropTypes.func, traceIndexes: PropTypes.array, + customConfig: PropTypes.object, + hasValidCustomConfigVisibilityRules: PropTypes.bool, }; export default function connectToContainer(WrappedComponent, config = {}) { @@ -44,7 +46,7 @@ export default function connectToContainer(WrappedComponent, config = {}) { } setLocals(props, context) { - this.plotProps = unpackPlotProps(props, context); + this.plotProps = unpackPlotProps(props, context, WrappedComponent); this.attr = props.attr; ContainerConnectedComponent.modifyPlotProps(props, context, this.plotProps); } @@ -62,7 +64,17 @@ export default function connectToContainer(WrappedComponent, config = {}) { // is also wrapped by a component that `unpackPlotProps`. That way inner // component can skip computation as it can see plotProps is already defined. const {plotProps = this.plotProps, ...props} = Object.assign({}, this.plotProps, this.props); - if (props.isVisible) { + const wrappedComponentDisplayName = + WrappedComponent && WrappedComponent.displayName ? WrappedComponent.displayName : null; + + if ( + isVisibleGivenCustomConfig( + props.isVisible, + props, + this.context, + wrappedComponentDisplayName + ) + ) { return ; } diff --git a/src/lib/index.js b/src/lib/index.js index 7c8baccf7..d9fce5f0d 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -23,7 +23,11 @@ import getAllAxes, { } from './getAllAxes'; import localize, {localizeString} from './localize'; import tinyColor from 'tinycolor2'; -import unpackPlotProps from './unpackPlotProps'; +import unpackPlotProps, { + computeCustomConfigVisibility, + hasValidCustomConfigVisibilityRules, + isVisibleGivenCustomConfig, +} from './unpackPlotProps'; import walkObject, {isPlainObject} from './walkObject'; import {traceTypeToPlotlyInitFigure, plotlyTraceToCustomTrace} from './customTraceType'; import * as PlotlyIcons from 'plotly-icons'; @@ -216,6 +220,7 @@ function getParsedTemplateString(originalString, context) { export { adjustColorscale, + computeCustomConfigVisibility, axisIdToAxisName, bem, camelCase, @@ -244,6 +249,7 @@ export { getFullTrace, getSubplotTitle, isPlainObject, + hasValidCustomConfigVisibilityRules, localize, localizeString, lowerCase, @@ -261,5 +267,6 @@ export { transpose, unpackPlotProps, upperCase, + isVisibleGivenCustomConfig, walkObject, }; diff --git a/src/lib/test-utils.js b/src/lib/test-utils.js index 2da40a86c..8b947fec0 100644 --- a/src/lib/test-utils.js +++ b/src/lib/test-utils.js @@ -104,7 +104,7 @@ function applyConfig(config = {}, {graphDiv: {data, layout}, dataSourceOptions, // replace simple graphDiv with properly mocked GD including fullData/fullLayout const graphDiv = setupGraphDiv({data, layout}); - return {dataSources, dataSourceOptions, graphDiv}; + return {dataSources, dataSourceOptions, graphDiv, customConfig: config.customConfig || {}}; } /* diff --git a/src/lib/unpackPlotProps.js b/src/lib/unpackPlotProps.js index 138707609..af917202f 100644 --- a/src/lib/unpackPlotProps.js +++ b/src/lib/unpackPlotProps.js @@ -1,7 +1,104 @@ +/* eslint-disable no-console */ + import nestedProperty from 'plotly.js/src/lib/nested_property'; import isNumeric from 'fast-isnumeric'; import {MULTI_VALUED, MULTI_VALUED_PLACEHOLDER} from './constants'; +const hasFullValue = fullValue => fullValue !== void 0 && fullValue !== null; + +export function hasValidCustomConfigVisibilityRules(customConfig) { + if ( + customConfig && + customConfig === Object(customConfig) && + Object.keys(customConfig).length && + customConfig.visibility_rules + ) { + if (customConfig.visibility_rules.blacklist && customConfig.visibility_rules.whitelist) { + console.error( + 'customConfig.visibility_rules can have a blacklist OR whitelist key, both are present in your config.' + ); + return false; + } + + if ( + !Object.keys(customConfig.visibility_rules).some(key => + ['blacklist', 'whitelist'].includes(key) + ) + ) { + console.error( + 'customConfig.visibility_rules must have at least a blacklist or whitelist key.' + ); + return false; + } + + const isValidRule = rule => { + if (rule.exceptions) { + return rule.exceptions.every(isValidRule); + } + return rule.type && ['attrName', 'controlType'].includes(rule.type) && rule.regex_match; + }; + + const errorMessage = + "All rules and exceptions must have a type (one of: 'attrName' or 'controlType') and regex_match key."; + + if ( + customConfig.visibility_rules.blacklist && + !customConfig.visibility_rules.blacklist.every(isValidRule) + ) { + console.error(errorMessage); + return false; + } + + if ( + customConfig.visibility_rules.whitelist && + !customConfig.visibility_rules.whitelist.every(isValidRule) + ) { + console.error(errorMessage); + return false; + } + + return true; + } + return false; +} + +export function computeCustomConfigVisibility(props, customConfig, wrappedComponentDisplayName) { + let isVisible; + + const isRegexMatch = rule => { + const stringToTest = rule.type === 'attrName' ? props.attr : wrappedComponentDisplayName; + return RegExp(rule.regex_match).test(stringToTest); + }; + + const passesTest = rule => { + const hasException = rule => { + if (rule.exceptions) { + return rule.exceptions.some(exception => passesTest(exception)); + } + return false; + }; + return isRegexMatch(rule) && !hasException(rule); + }; + + if (customConfig.visibility_rules.blacklist) { + isVisible = !customConfig.visibility_rules.blacklist.some(passesTest); + } + + if (customConfig.visibility_rules.whitelist) { + isVisible = customConfig.visibility_rules.whitelist.some(passesTest); + } + + return isVisible; +} + +export function isVisibleGivenCustomConfig(initial, nextProps, nextContext, componentDisplayName) { + let show = initial; + if (show && nextContext.hasValidCustomConfigVisibilityRules) { + show = computeCustomConfigVisibility(nextProps, nextContext.customConfig, componentDisplayName); + } + return show; +} + export default function unpackPlotProps(props, context) { const {container, getValObject, defaultContainer, updateContainer} = context; @@ -28,10 +125,7 @@ export default function unpackPlotProps(props, context) { multiValued = true; } - let isVisible = false; - if (props.show || (fullValue !== void 0 && fullValue !== null)) { - isVisible = true; - } + const isVisible = Boolean(hasFullValue(fullValue) || props.show); let defaultValue = props.defaultValue; if (defaultValue === void 0 && defaultContainer) {