diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx
new file mode 100644
index 000000000..c58b3d382
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx
@@ -0,0 +1,223 @@
+/**
+ * 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.
+ */
+/* eslint-disable camelcase */
+import React, { useCallback, useMemo, useState } from 'react';
+import Tabs from 'src/components/Tabs';
+import Button from 'src/components/Button';
+import { NativeSelect as Select } from 'src/components/Select';
+import { t, styled } from '@superset-ui/core';
+
+import { Form, FormItem } from 'src/components/Form';
+import { StyledColumnOption } from 'src/explore/components/optionRenderers';
+import { ColumnMeta } from '@superset-ui/chart-controls';
+
+const StyledSelect = styled(Select)`
+ .metric-option {
+ & > svg {
+ min-width: ${({ theme }) => `${theme.gridUnit * 4}px`};
+ }
+ & > .option-label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+`;
+
+interface ColumnSelectPopoverProps {
+ columns: ColumnMeta[];
+ editedColumn?: ColumnMeta;
+ onChange: (column: ColumnMeta) => void;
+ onClose: () => void;
+}
+
+const ColumnSelectPopover = ({
+ columns,
+ editedColumn,
+ onChange,
+ onClose,
+}: ColumnSelectPopoverProps) => {
+ const [
+ initialCalculatedColumn,
+ initialSimpleColumn,
+ ] = editedColumn?.expression
+ ? [editedColumn, undefined]
+ : [undefined, editedColumn];
+ const [selectedCalculatedColumn, setSelectedCalculatedColumn] = useState(
+ initialCalculatedColumn,
+ );
+ const [selectedSimpleColumn, setSelectedSimpleColumn] = useState(
+ initialSimpleColumn,
+ );
+
+ const [calculatedColumns, simpleColumns] = useMemo(
+ () =>
+ columns?.reduce(
+ (acc: [ColumnMeta[], ColumnMeta[]], column: ColumnMeta) => {
+ if (column.expression) {
+ acc[0].push(column);
+ } else {
+ acc[1].push(column);
+ }
+ return acc;
+ },
+ [[], []],
+ ),
+ [columns],
+ );
+
+ const onCalculatedColumnChange = useCallback(
+ selectedColumnName => {
+ const selectedColumn = calculatedColumns.find(
+ col => col.column_name === selectedColumnName,
+ );
+ setSelectedCalculatedColumn(selectedColumn);
+ setSelectedSimpleColumn(undefined);
+ },
+ [calculatedColumns],
+ );
+
+ const onSimpleColumnChange = useCallback(
+ selectedColumnName => {
+ const selectedColumn = simpleColumns.find(
+ col => col.column_name === selectedColumnName,
+ );
+ setSelectedCalculatedColumn(undefined);
+ setSelectedSimpleColumn(selectedColumn);
+ },
+ [simpleColumns],
+ );
+
+ const defaultActiveTabKey =
+ initialSimpleColumn || calculatedColumns.length === 0 ? 'simple' : 'saved';
+
+ const onSave = useCallback(() => {
+ const selectedColumn = selectedCalculatedColumn || selectedSimpleColumn;
+ if (!selectedColumn) {
+ return;
+ }
+ onChange(selectedColumn);
+ onClose();
+ }, [onChange, onClose, selectedCalculatedColumn, selectedSimpleColumn]);
+
+ const onResetStateAndClose = useCallback(() => {
+ setSelectedCalculatedColumn(initialCalculatedColumn);
+ setSelectedSimpleColumn(initialSimpleColumn);
+ onClose();
+ }, [initialCalculatedColumn, initialSimpleColumn, onClose]);
+
+ const stateIsValid = selectedCalculatedColumn || selectedSimpleColumn;
+ const hasUnsavedChanges =
+ selectedCalculatedColumn?.column_name !==
+ initialCalculatedColumn?.column_name ||
+ selectedSimpleColumn?.column_name !== initialSimpleColumn?.column_name;
+
+ const filterOption = useCallback(
+ (input, option) =>
+ option?.filterBy.toLowerCase().indexOf(input.toLowerCase()) >= 0,
+ [],
+ );
+
+ const getPopupContainer = useCallback(
+ (triggerNode: any) => triggerNode.parentNode,
+ [],
+ );
+
+ return (
+
+ );
+};
+
+export default ColumnSelectPopover;
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopoverTrigger.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopoverTrigger.tsx
new file mode 100644
index 000000000..4caf086d4
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopoverTrigger.tsx
@@ -0,0 +1,99 @@
+/**
+ * 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 React, { useCallback, useMemo, useState } from 'react';
+import { ColumnMeta } from '@superset-ui/chart-controls';
+import Popover from 'src/components/Popover';
+import { ExplorePopoverContent } from 'src/explore/components/ExploreContentPopover';
+import ColumnSelectPopover from './ColumnSelectPopover';
+
+interface ColumnSelectPopoverTriggerProps {
+ columns: ColumnMeta[];
+ editedColumn?: ColumnMeta;
+ onColumnEdit: (editedColumn: ColumnMeta) => void;
+ isControlledComponent?: boolean;
+ visible?: boolean;
+ togglePopover?: (visible: boolean) => void;
+ closePopover?: () => void;
+ children: React.ReactNode;
+}
+
+const ColumnSelectPopoverTrigger = ({
+ columns,
+ editedColumn,
+ onColumnEdit,
+ isControlledComponent,
+ children,
+ ...props
+}: ColumnSelectPopoverTriggerProps) => {
+ const [popoverVisible, setPopoverVisible] = useState(false);
+
+ const togglePopover = useCallback((visible: boolean) => {
+ setPopoverVisible(visible);
+ }, []);
+
+ const closePopover = useCallback(() => {
+ setPopoverVisible(false);
+ }, []);
+
+ const {
+ visible,
+ handleTogglePopover,
+ handleClosePopover,
+ } = isControlledComponent
+ ? {
+ visible: props.visible,
+ handleTogglePopover: props.togglePopover!,
+ handleClosePopover: props.closePopover!,
+ }
+ : {
+ visible: popoverVisible,
+ handleTogglePopover: togglePopover,
+ handleClosePopover: closePopover,
+ };
+
+ const overlayContent = useMemo(
+ () => (
+
+
+
+ ),
+ [columns, editedColumn, handleClosePopover, onColumnEdit],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default ColumnSelectPopoverTrigger;
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.tsx
index 788b4e27c..69cc6442e 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.tsx
@@ -16,8 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
-import React, { useCallback, useMemo } from 'react';
-import { tn } from '@superset-ui/core';
+import React, { useCallback, useMemo, useState } from 'react';
+import { FeatureFlag, isFeatureEnabled, tn } from '@superset-ui/core';
import { ColumnMeta } from '@superset-ui/chart-controls';
import { isEmpty } from 'lodash';
import { LabelProps } from 'src/explore/components/controls/DndColumnSelectControl/types';
@@ -27,6 +27,7 @@ import { OptionSelector } from 'src/explore/components/controls/DndColumnSelectC
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types';
import { DndItemType } from 'src/explore/components/DndItemType';
import { useComponentDidUpdate } from 'src/common/hooks/useComponentDidUpdate';
+import ColumnSelectPopoverTrigger from './ColumnSelectPopoverTrigger';
export const DndColumnSelect = (props: LabelProps) => {
const {
@@ -39,6 +40,8 @@ export const DndColumnSelect = (props: LabelProps) => {
name,
label,
} = props;
+ const [newColumnPopoverVisible, setNewColumnPopoverVisible] = useState(false);
+
const optionSelector = useMemo(
() => new OptionSelector(options, multi, value),
[multi, options, value],
@@ -110,41 +113,120 @@ export const DndColumnSelect = (props: LabelProps) => {
[onChange, optionSelector],
);
+ const popoverOptions = useMemo(
+ () =>
+ Object.values(options).filter(
+ col =>
+ !optionSelector.values
+ .map(val => val.column_name)
+ .includes(col.column_name),
+ ),
+ [optionSelector.values, options],
+ );
+
const valuesRenderer = useCallback(
() =>
- optionSelector.values.map((column, idx) => (
-
- )),
+ optionSelector.values.map((column, idx) =>
+ isFeatureEnabled(FeatureFlag.ENABLE_DND_WITH_CLICK_UX) ? (
+ {
+ optionSelector.replace(idx, newColumn.column_name);
+ onChange(optionSelector.getValues());
+ }}
+ editedColumn={column}
+ >
+
+
+ ) : (
+
+ ),
+ ),
[
canDelete,
label,
name,
+ onChange,
onClickClose,
onShiftOptions,
- optionSelector.values,
+ optionSelector,
+ popoverOptions,
],
);
+ const addNewColumnWithPopover = useCallback(
+ (newColumn: ColumnMeta) => {
+ optionSelector.add(newColumn.column_name);
+ onChange(optionSelector.getValues());
+ },
+ [onChange, optionSelector],
+ );
+
+ const togglePopover = useCallback((visible: boolean) => {
+ setNewColumnPopoverVisible(visible);
+ }, []);
+
+ const closePopover = useCallback(() => {
+ togglePopover(false);
+ }, [togglePopover]);
+
+ const openPopover = useCallback(() => {
+ togglePopover(true);
+ }, [togglePopover]);
+
+ const defaultGhostButtonText = isFeatureEnabled(
+ FeatureFlag.ENABLE_DND_WITH_CLICK_UX,
+ )
+ ? tn(
+ 'Drop a column here or click',
+ 'Drop columns here or click',
+ multi ? 2 : 1,
+ )
+ : tn('Drop column here', 'Drop columns here', multi ? 2 : 1);
+
return (
-
- onDrop={onDrop}
- canDrop={canDrop}
- valuesRenderer={valuesRenderer}
- accept={DndItemType.Column}
- displayGhostButton={multi || optionSelector.values.length === 0}
- ghostButtonText={
- ghostButtonText ||
- tn('Drop column here', 'Drop columns here', multi ? 2 : 1)
- }
- {...props}
- />
+
+
+ onDrop={onDrop}
+ canDrop={canDrop}
+ valuesRenderer={valuesRenderer}
+ accept={DndItemType.Column}
+ displayGhostButton={multi || optionSelector.values.length === 0}
+ ghostButtonText={ghostButtonText || defaultGhostButtonText}
+ onClickGhostButton={
+ isFeatureEnabled(FeatureFlag.ENABLE_DND_WITH_CLICK_UX)
+ ? openPopover
+ : undefined
+ }
+ {...props}
+ />
+
+
+
+
);
};
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx
index 1e2eba8a6..97842f5bb 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx
@@ -333,6 +333,11 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
],
);
+ const handleClickGhostButton = useCallback(() => {
+ setDroppedItem(null);
+ togglePopover(true);
+ }, [togglePopover]);
+
const adhocFilter = useMemo(() => {
if (droppedItem?.metric_name) {
return new AdhocFilter({
@@ -351,7 +356,7 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
const config: Partial = {
subject: (droppedItem as ColumnMeta)?.column_name,
};
- if (isFeatureEnabled(FeatureFlag.UX_BETA)) {
+ if (config.subject && isFeatureEnabled(FeatureFlag.UX_BETA)) {
config.operator = OPERATOR_ENUM_TO_OPERATOR_TYPE[Operators.IN].operation;
config.operatorId = Operators.IN;
}
@@ -367,6 +372,10 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
[togglePopover],
);
+ const ghostButtonText = isFeatureEnabled(FeatureFlag.ENABLE_DND_WITH_CLICK_UX)
+ ? t('Drop columns/metrics here or click')
+ : t('Drop columns or metrics here');
+
return (
<>
@@ -374,7 +383,12 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
canDrop={canDrop}
valuesRenderer={valuesRenderer}
accept={DND_ACCEPTED_TYPES}
- ghostButtonText={t('Drop columns or metrics here')}
+ ghostButtonText={ghostButtonText}
+ onClickGhostButton={
+ isFeatureEnabled(FeatureFlag.ENABLE_DND_WITH_CLICK_UX)
+ ? handleClickGhostButton
+ : undefined
+ }
{...props}
/>
{
[onNewMetric, togglePopover],
);
+ const handleClickGhostButton = useCallback(() => {
+ setDroppedItem(null);
+ togglePopover(true);
+ }, [togglePopover]);
+
const adhocMetric = useMemo(() => {
if (droppedItem?.type === DndItemType.Column) {
const itemValue = droppedItem?.value as ColumnMeta;
@@ -334,6 +339,18 @@ export const DndMetricSelect = (props: any) => {
return new AdhocMetric({ isNew: true });
}, [droppedItem]);
+ const ghostButtonText = isFeatureEnabled(FeatureFlag.ENABLE_DND_WITH_CLICK_UX)
+ ? tn(
+ 'Drop a column/metric here or click',
+ 'Drop columns/metrics here or click',
+ multi ? 2 : 1,
+ )
+ : tn(
+ 'Drop column or metric here',
+ 'Drop columns or metrics here',
+ multi ? 2 : 1,
+ );
+
return (
@@ -341,12 +358,13 @@ export const DndMetricSelect = (props: any) => {
canDrop={canDrop}
valuesRenderer={valuesRenderer}
accept={DND_ACCEPTED_TYPES}
- ghostButtonText={tn(
- 'Drop column or metric here',
- 'Drop columns or metrics here',
- multi ? 2 : 1,
- )}
+ ghostButtonText={ghostButtonText}
displayGhostButton={multi || value.length === 0}
+ onClickGhostButton={
+ isFeatureEnabled(FeatureFlag.ENABLE_DND_WITH_CLICK_UX)
+ ? handleClickGhostButton
+ : undefined
+ }
{...props}
/>
({
function renderGhostButton() {
return (
-
+
{t(props.ghostButtonText || 'Drop columns here')}
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts
index 6a8a6876d..b77e9d263 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts
@@ -65,6 +65,7 @@ export interface DndColumnSelectProps<
accept: DndItemType | DndItemType[];
ghostButtonText?: string;
displayGhostButton?: boolean;
+ onClickGhostButton?: () => void;
}
export type OptionValueType = Record;
diff --git a/superset/config.py b/superset/config.py
index 58d9693f6..f18fcca8a 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -388,6 +388,7 @@ DEFAULT_FEATURE_FLAGS: Dict[str, bool] = {
"OMNIBAR": False,
"DASHBOARD_RBAC": False,
"ENABLE_EXPLORE_DRAG_AND_DROP": False,
+ "ENABLE_DND_WITH_CLICK_UX": False,
# Enabling ALERTS_ATTACH_REPORTS, the system sends email and slack message
# with screenshot and link
# Disables ALERTS_ATTACH_REPORTS, the system DOES NOT generate screenshot