diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.ts b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.tsx similarity index 81% rename from superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.ts rename to superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.tsx index 9425f1211701..a4d7d689e7c0 100644 --- a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.ts +++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controlPanel.tsx @@ -21,6 +21,7 @@ import { ControlPanelConfig, getStandardizedControls, } from '@superset-ui/chart-controls'; +import { RotationControl, ColorSchemeControl } from './controls'; const config: ControlPanelConfig = { controlPanelSections: [ @@ -63,25 +64,14 @@ const config: ControlPanelConfig = { }, }, ], + [], [ - { - name: 'rotation', - config: { - type: 'SelectControl', - label: t('Word Rotation'), - choices: [ - ['random', t('random')], - ['flat', t('flat')], - ['square', t('square')], - ], - renderTrigger: true, - default: 'square', - clearable: false, - description: t('Rotation to apply to words in the cloud'), - }, - }, + , ], - ['color_scheme'], ], }, ], diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl.tsx b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl.tsx new file mode 100644 index 000000000000..f9f1859b1ffd --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl.tsx @@ -0,0 +1,62 @@ +/** + * 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. + */ +import { getCategoricalSchemeRegistry } from '@superset-ui/core'; +import InternalColorSchemeControl from './ColorSchemeControl/index'; +import { ColorSchemes } from './ColorSchemeControl/index'; +// NOTE: We copied the Explore ColorSchemeControl into this plugin to avoid +// pulling the entire frontend src tree into this package’s tsconfig (importing +// from src/ was dragging in fixtures, tests, and other plugins). Keep this copy +// in sync with upstream changes, and consider moving it into a shared package +// once the control-panel refactor settles so all consumers can reuse it. +import { ControlComponentProps } from '@superset-ui/chart-controls'; + +type ColorSchemeControlWrapperProps = ControlComponentProps & { + clearable?: boolean; +}; + +export default function ColorSchemeControlWrapper({ + name = 'color_scheme', + value, + onChange, + clearable = true, + label, + description, + ...rest +}: ColorSchemeControlWrapperProps) { + const categoricalSchemeRegistry = getCategoricalSchemeRegistry(); + const choices = categoricalSchemeRegistry.keys().map(s => [s, s]); + const schemes = categoricalSchemeRegistry.getMap() as ColorSchemes; + + return ( + + ); +} + +ColorSchemeControlWrapper.displayName = 'ColorSchemeControlWrapper'; diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl/ColorSchemeLabel.tsx b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl/ColorSchemeLabel.tsx new file mode 100644 index 000000000000..135d7d1134b3 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl/ColorSchemeLabel.tsx @@ -0,0 +1,126 @@ +/** + * 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. + */ + +import { css, SupersetTheme } from '@apache-superset/core/ui'; +import { useRef, useState } from 'react'; +import { Tooltip } from '@superset-ui/core/components'; + +type ColorSchemeLabelProps = { + colors: string[]; + id: string; + label: string; +}; + +export default function ColorSchemeLabel(props: ColorSchemeLabelProps) { + const { id, label, colors } = props; + const [showTooltip, setShowTooltip] = useState(false); + const labelNameRef = useRef(null); + const labelsColorRef = useRef(null); + const handleShowTooltip = () => { + const labelNameElement = labelNameRef.current; + const labelsColorElement = labelsColorRef.current; + if ( + labelNameElement && + labelsColorElement && + (labelNameElement.scrollWidth > labelNameElement.offsetWidth || + labelNameElement.scrollHeight > labelNameElement.offsetHeight || + labelsColorElement.scrollWidth > labelsColorElement.offsetWidth || + labelsColorElement.scrollHeight > labelsColorElement.offsetHeight) + ) { + setShowTooltip(true); + } + }; + const handleHideTooltip = () => { + setShowTooltip(false); + }; + + const colorsList = () => + colors.map((color: string, i: number) => ( + css` + padding-left: ${theme.sizeUnit / 2}px; + :before { + content: ''; + display: inline-block; + background-color: ${color}; + border: 1px solid ${color === 'white' ? 'black' : color}; + width: 9px; + height: 10px; + } + `} + /> + )); + + const tooltipContent = () => ( + <> + {label} +
{colorsList()}
+ + ); + + return ( + + + css` + min-width: 125px; + padding-right: ${theme.sizeUnit * 2}px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + `} + > + {label} + + css` + flex: 100%; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + padding-right: ${theme.sizeUnit}px; + `} + > + {colorsList()} + + + + ); +} diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl/index.tsx b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl/index.tsx new file mode 100644 index 000000000000..22983ca28629 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/ColorSchemeControl/index.tsx @@ -0,0 +1,333 @@ +/** + * 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. + */ +import { useMemo, ReactNode } from 'react'; + +import { + ColorScheme, + ColorSchemeGroup, + SequentialScheme, + t, + getLabelsColorMap, + CategoricalColorNamespace, +} from '@superset-ui/core'; +import { css, useTheme } from '@apache-superset/core/ui'; +import { sortBy } from 'lodash'; +import { ControlHeader } from '@superset-ui/chart-controls'; +import { + Tooltip, + Select, + type SelectOptionsType, +} from '@superset-ui/core/components'; +import { Icons } from '@superset-ui/core/components/Icons'; +import ColorSchemeLabel from './ColorSchemeLabel'; + +const getColorNamespace = (namespace?: string) => namespace || undefined; + +export type OptionData = SelectOptionsType[number]['options'][number] & { + searchText?: string; +}; + +export interface ColorSchemes { + [key: string]: ColorScheme; +} + +export interface ColorSchemeControlProps { + hasCustomLabelsColor: boolean; + hasDashboardColorScheme?: boolean; + hasSharedLabelsColor?: boolean; + sharedLabelsColors?: string[]; + mapLabelsColors?: Record; + colorNamespace?: string; + chartId?: number; + dashboardId?: number; + label?: string; + name: string; + onChange?: (value: string) => void; + value: string; + clearable: boolean; + defaultScheme?: string; + choices: string[][] | (() => string[][]); + schemes: ColorSchemes | (() => ColorSchemes); + isLinear?: boolean; + description?: string; + hovered?: boolean; +} + +const CUSTOM_LABEL_ALERT = t( + `The colors of this chart might be overridden by custom label colors of the related dashboard. + Check the JSON metadata in the Advanced settings.`, +); + +const DASHBOARD_ALERT = t( + `The color scheme is determined by the related dashboard. + Edit the color scheme in the dashboard properties.`, +); + +const DASHBOARD_CONTEXT_ALERT = t( + `You are viewing this chart in a dashboard context with labels shared across multiple charts. + The color scheme selection is disabled.`, +); + +const DASHBOARD_CONTEXT_TOOLTIP = t( + `You are viewing this chart in the context of a dashboard that is directly affecting its colors. + To edit the color scheme, open this chart outside of the dashboard.`, +); + +const Label = ({ + label, + dashboardId, + hasSharedLabelsColor, + hasCustomLabelsColor, + hasDashboardColorScheme, +}: Pick< + ColorSchemeControlProps, + | 'label' + | 'dashboardId' + | 'hasCustomLabelsColor' + | 'hasSharedLabelsColor' + | 'hasDashboardColorScheme' +>) => { + const theme = useTheme(); + if (hasSharedLabelsColor || hasCustomLabelsColor || hasDashboardColorScheme) { + const alertTitle = + hasCustomLabelsColor && !hasSharedLabelsColor + ? CUSTOM_LABEL_ALERT + : dashboardId && hasDashboardColorScheme + ? DASHBOARD_ALERT + : DASHBOARD_CONTEXT_ALERT; + return ( + <> + {label}{' '} + + + + + ); + } + return <>{label}; +}; + +const ColorSchemeControl = ({ + hasCustomLabelsColor = false, + hasDashboardColorScheme = false, + mapLabelsColors = {}, + sharedLabelsColors = [], + dashboardId, + colorNamespace, + chartId, + label = t('Color scheme'), + onChange = () => {}, + value, + clearable = false, + defaultScheme, + choices = [], + schemes = {}, + isLinear, + ...rest +}: ColorSchemeControlProps) => { + const countSharedLabelsColor = sharedLabelsColors.length; + const colorMapInstance = getLabelsColorMap(); + const chartLabels = chartId + ? colorMapInstance.chartsLabelsMap.get(chartId)?.labels || [] + : []; + const hasSharedLabelsColor = !!( + dashboardId && + countSharedLabelsColor > 0 && + chartLabels.some(label => sharedLabelsColors.includes(label)) + ); + const hasDashboardScheme = dashboardId && hasDashboardColorScheme; + const showDashboardLockedOption = hasDashboardScheme || hasSharedLabelsColor; + const theme = useTheme(); + const currentScheme = useMemo(() => { + if (showDashboardLockedOption) { + return 'dashboard'; + } + let result = value || defaultScheme; + if (result === 'SUPERSET_DEFAULT') { + const schemesObject = typeof schemes === 'function' ? schemes() : schemes; + result = schemesObject?.SUPERSET_DEFAULT?.id; + } + return result; + }, [defaultScheme, schemes, showDashboardLockedOption, value]); + + const options = useMemo(() => { + if (showDashboardLockedOption) { + return [ + { + value: 'dashboard', + label: ( + + {t('Dashboard scheme')} + + ), + }, + ]; + } + const schemesObject = typeof schemes === 'function' ? schemes() : schemes; + const controlChoices = typeof choices === 'function' ? choices() : choices; + const allColorOptions: string[] = []; + const filteredColorOptions = controlChoices.filter(o => { + const option = o[0]; + const isValidColorOption = + option !== 'SUPERSET_DEFAULT' && !allColorOptions.includes(option); + allColorOptions.push(option); + return isValidColorOption; + }); + + const groups = filteredColorOptions.reduce( + (acc, [value]) => { + const currentScheme = schemesObject[value]; + + // For categorical scheme, display all the colors + // For sequential scheme, show 10 or interpolate to 10. + // Sequential schemes usually have at most 10 colors. + let colors: string[] = []; + if (currentScheme) { + colors = isLinear + ? (currentScheme as SequentialScheme).getColors(10) + : currentScheme.colors; + } + const option = { + label: ( + + ) as ReactNode, + value, + searchText: currentScheme.label, + }; + acc[currentScheme.group ?? ColorSchemeGroup.Other].options.push(option); + return acc; + }, + { + [ColorSchemeGroup.Custom]: { + title: ColorSchemeGroup.Custom, + label: t('Custom color palettes'), + options: [] as OptionData[], + }, + [ColorSchemeGroup.Featured]: { + title: ColorSchemeGroup.Featured, + label: t('Featured color palettes'), + options: [] as OptionData[], + }, + [ColorSchemeGroup.Other]: { + title: ColorSchemeGroup.Other, + label: t('Other color palettes'), + options: [] as OptionData[], + }, + }, + ); + const nonEmptyGroups = Object.values(groups) + .filter(group => group.options.length > 0) + .map(group => ({ + ...group, + options: sortBy(group.options, opt => opt.label), + })); + + // if there are no featured or custom color schemes, return the ungrouped options + if ( + nonEmptyGroups.length === 1 && + nonEmptyGroups[0].title === ColorSchemeGroup.Other + ) { + return nonEmptyGroups[0].options.map(opt => ({ + value: opt.value, + label: opt.customLabel || opt.label, + })); + } + return nonEmptyGroups.map(group => ({ + label: group.label, + options: group.options.map(opt => ({ + value: opt.value, + label: opt.customLabel || opt.label, + searchText: opt.searchText, + })), + })); + }, [choices, hasDashboardScheme, hasSharedLabelsColor, isLinear, schemes]); + + // We can't pass on change directly because it receives a second + // parameter and it would be interpreted as the error parameter + const handleOnChange = (value: string) => { + if (chartId) { + colorMapInstance.setOwnColorScheme(chartId, value); + if (dashboardId) { + const colorNameSpace = getColorNamespace(colorNamespace); + const categoricalNamespace = + CategoricalColorNamespace.getNamespace(colorNameSpace); + + const sharedLabelsSet = new Set(sharedLabelsColors); + // reset colors except shared and custom labels to keep dashboard consistency + const resettableLabels = Object.keys(mapLabelsColors).filter( + l => !sharedLabelsSet.has(l), + ); + categoricalNamespace.resetColorsForLabels(resettableLabels); + } + } + + onChange(value); + }; + + return ( + <> + + } + /> + ({ label: text, value: key }))} + onChange={(val: SelectValue) => { + if (val === null || val === undefined) { + return; + } + // Handle LabeledValue object + if ( + typeof val === 'object' && + 'value' in val && + val.value !== undefined + ) { + onChange?.(val.value as string); + } else if (typeof val === 'string' || typeof val === 'number') { + // Handle raw value + onChange?.(String(val)); + } + }} + allowClear={clearable} + /> + + ); +} + +RotationControl.displayName = 'RotationControl'; diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/index.ts b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/index.ts new file mode 100644 index 000000000000..ef7ad329b174 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-word-cloud/src/plugin/controls/index.ts @@ -0,0 +1,20 @@ +/** + * 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. + */ +export { default as RotationControl } from './RotationControl'; +export { default as ColorSchemeControl } from './ColorSchemeControl'; diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/test/ColorSchemeControl.test.tsx b/superset-frontend/plugins/plugin-chart-word-cloud/test/ColorSchemeControl.test.tsx new file mode 100644 index 000000000000..014895cf8c59 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-word-cloud/test/ColorSchemeControl.test.tsx @@ -0,0 +1,83 @@ +/** + * 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. + */ + +import { ColorSchemeControl } from '../src/plugin/controls'; +import { render, screen, userEvent } from 'spec/helpers/testing-library'; + +const setup = (props = {}) => { + const defaultProps = { + name: 'color_scheme', + value: '', + onChange: jest.fn(), + }; + return render(); +}; + +test('renders color scheme control', () => { + setup(); + // The Select component has an aria-label - use role to find the input specifically + expect( + screen.getByRole('combobox', { name: 'Select color scheme' }), + ).toBeInTheDocument(); +}); + +test('renders select with value', () => { + // Get a color scheme from the registry to use as a test value + const { getCategoricalSchemeRegistry } = require('@superset-ui/core'); + const registry = getCategoricalSchemeRegistry(); + const firstScheme = registry.keys()[0]; + + setup({ value: firstScheme }); + // Use role to find the input specifically + const select = screen.getByRole('combobox', { name: 'Select color scheme' }); + expect(select).toBeInTheDocument(); +}); + +test('calls onChange when value changes', async () => { + const onChange = jest.fn(); + const { getCategoricalSchemeRegistry } = require('@superset-ui/core'); + const registry = getCategoricalSchemeRegistry(); + const schemes = registry.keys(); + + if (schemes.length < 2) { + // Skip if there aren't enough schemes to test + return; + } + + const initialScheme = schemes[0]; + const newScheme = schemes[1]; + + setup({ onChange, value: initialScheme }); + + // Find the select input using role + const selectInput = screen.getByRole('combobox', { + name: 'Select color scheme', + }); + + userEvent.click(selectInput); + + // Wait for and select a different color scheme + // The scheme name should be visible in the dropdown + const newSchemeOption = await screen.findByText(newScheme, { exact: false }); + userEvent.click(newSchemeOption); + + // Verify onChange was called with the new scheme value + expect(onChange).toHaveBeenCalledWith(newScheme); + expect(onChange).toHaveBeenCalledTimes(1); +}); diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/test/RotationControl.test.tsx b/superset-frontend/plugins/plugin-chart-word-cloud/test/RotationControl.test.tsx new file mode 100644 index 000000000000..b10999a56db0 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-word-cloud/test/RotationControl.test.tsx @@ -0,0 +1,59 @@ +/** + * 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. + */ + +import { RotationControl } from '../src/plugin/controls'; +import { render, screen, userEvent } from 'spec/helpers/testing-library'; + +const setup = (props = {}) => { + const defaultProps = { + name: 'rotation', + value: 'square', + onChange: jest.fn(), + }; + return render(); +}; + +test('renders rotation control with label', () => { + setup(); + expect(screen.getByText('Word Rotation')).toBeInTheDocument(); +}); + +test('renders select with default value', () => { + setup({ value: 'flat' }); + // Check that the select is rendered (implementation depends on Select component) + expect(screen.getByRole('combobox')).toBeInTheDocument(); +}); + +test('calls onChange when value changes', async () => { + const onChange = jest.fn(); + setup({ onChange, value: 'square' }); + + // Find the select input and open the dropdown + const selectInput = screen.getByRole('combobox'); + + await userEvent.click(selectInput); + + // Wait for and select a different option + const flatOption = await screen.findByText('flat', { exact: false }); + await userEvent.click(flatOption); + + // Verify onChange was called with the string value + expect(onChange).toHaveBeenCalledWith('flat'); + expect(onChange).toHaveBeenCalledTimes(1); +}); diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/test/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-word-cloud/test/controlPanel.test.ts new file mode 100644 index 000000000000..56794dd5f298 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-word-cloud/test/controlPanel.test.ts @@ -0,0 +1,47 @@ +/** + * 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. + */ +import controlPanel from '../src/plugin/controlPanel'; +import React, { ReactElement } from 'react'; + +const isNameControl = ( + item: unknown, + name: string, +): item is ReactElement<{ name: string }> => + React.isValidElement<{ name: string }>(item) && item.props.name === name; + +test('control panel has rotation and color_scheme controls', () => { + const optionsSection = controlPanel.controlPanelSections.find( + (section): section is NonNullable => + Boolean(section && section.label === 'Options'), + ); + expect(optionsSection).toBeDefined(); + if (!optionsSection) { + throw new Error('Options section missing'); + } + + const rotationRow = optionsSection.controlSetRows.find(row => + row.some(item => isNameControl(item, 'rotation')), + ); + expect(rotationRow).toBeDefined(); + + const colorSchemeRow = optionsSection.controlSetRows.find(row => + row.some(item => isNameControl(item, 'color_scheme')), + ); + expect(colorSchemeRow).toBeDefined(); +}); diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/test/tsconfig.json b/superset-frontend/plugins/plugin-chart-word-cloud/test/tsconfig.json index 42fa49ba5149..be84a09330ca 100644 --- a/superset-frontend/plugins/plugin-chart-word-cloud/test/tsconfig.json +++ b/superset-frontend/plugins/plugin-chart-word-cloud/test/tsconfig.json @@ -1,8 +1,7 @@ { "compilerOptions": { "composite": false, - "emitDeclarationOnly": false, - "rootDir": "." + "emitDeclarationOnly": false }, "extends": "../../../tsconfig.json", "include": ["**/*", "../types/**/*", "../../../types/**/*"] diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx index 6526201ac8d0..1b1e5046b2c9 100644 --- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx +++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx @@ -18,6 +18,7 @@ */ /* eslint camelcase: 0 */ import { + cloneElement, isValidElement, ReactNode, useCallback, @@ -575,7 +576,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { const renderControlPanelSection = ( section: ExpandedControlPanelSectionConfig, ) => { - const { controls } = props; + const { controls, chart, exploreState, form_data, actions } = props; const { label, description, visibility } = section; // Section label can be a ReactNode but in some places we want to @@ -657,7 +658,32 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { } if (isValidElement(controlItem)) { // When the item is a React element - return controlItem; + const element = controlItem as React.ReactElement< + Record + >; + + const controlName = (element.props as { name: string }) + .name; + if (!controlName) { + return element; + } + const controlState = controls[controlName]; + + return cloneElement(element, { + ...(element.props as Record), + actions, + controls, + chart, + exploreState, + form_data, + ...(controlState && { + value: controlState.value, + validationErrors: controlState.validationErrors, + default: controlState.default, + onChange: (value: unknown, errors: unknown[]) => + setControlValue(controlName, value, errors), + }), + }); } if ( isCustomControlItem(controlItem) && diff --git a/superset-frontend/src/explore/controlUtils/getControlState.ts b/superset-frontend/src/explore/controlUtils/getControlState.ts index be80aae1a809..4a9e139ec4d6 100644 --- a/superset-frontend/src/explore/controlUtils/getControlState.ts +++ b/superset-frontend/src/explore/controlUtils/getControlState.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { DatasourceType, ensureIsArray, @@ -182,6 +182,14 @@ export function getAllControlsState( state, formData[name], ); + } else if (React.isValidElement(field)) { + const props = field.props as { name: string; [key: string]: any }; + const { name, ...configProps } = props; + controlsState[name] = getControlStateFromControlConfig( + configProps as ControlConfig, + state, + formData[name], + ); } }), ); diff --git a/superset-frontend/src/utils/getControlsForVizType.ts b/superset-frontend/src/utils/getControlsForVizType.ts index 4fff9a06f15e..44b9045f5d37 100644 --- a/superset-frontend/src/utils/getControlsForVizType.ts +++ b/superset-frontend/src/utils/getControlsForVizType.ts @@ -18,6 +18,7 @@ */ import memoizeOne from 'memoize-one'; +import React from 'react'; import { isControlPanelSectionConfig } from '@superset-ui/chart-controls'; import { getChartControlPanelRegistry, JsonObject } from '@superset-ui/core'; import type { ControlMap } from 'src/components/AlteredSliceTag/types'; @@ -56,6 +57,17 @@ const memoizedControls = memoizeOne( config: JsonObject; }; controlsMap[controlObj.name] = controlObj.config; + } else if (React.isValidElement(control)) { + const { name } = control.props as { name: string }; + if (name) { + const ComponentType = control.type as React.ComponentType; + controlsMap[name] = { + type: + ComponentType.displayName || + ComponentType.name || + 'CustomControl', + }; + } } }); }