+ );
+}
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/components/Option.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/components/Option.tsx
new file mode 100644
index 000000000000..22cbb037a13a
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/components/Option.tsx
@@ -0,0 +1,53 @@
+/**
+ * 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 from 'react';
+import { useTheme } from '@superset-ui/core';
+import { ColumnOption } from '@superset-ui/chart-controls';
+import Icon from 'src/components/Icon';
+import {
+ CaretContainer,
+ CloseContainer,
+ OptionControlContainer,
+ Label,
+} from 'src/explore/components/OptionControls';
+import { OptionProps } from '../types';
+
+export default function Option(props: OptionProps) {
+ const theme = useTheme();
+
+ return (
+
+ props.clickClose(props.index)}
+ >
+
+
+
+ {props.withCaret && (
+
+
+
+ )}
+
+ );
+}
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/components/OptionWrapper.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/components/OptionWrapper.tsx
new file mode 100644
index 000000000000..13e18aadcfb0
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/components/OptionWrapper.tsx
@@ -0,0 +1,96 @@
+/**
+ * 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, { useRef } from 'react';
+import {
+ useDrag,
+ useDrop,
+ DropTargetMonitor,
+ DragSourceMonitor,
+} from 'react-dnd';
+import { DragContainer } from 'src/explore/components/OptionControls';
+import Option from './Option';
+import { OptionProps, GroupByItemInterface, GroupByItemType } from '../types';
+
+export default function OptionWrapper(props: OptionProps) {
+ const { index, onShiftOptions } = props;
+ const ref = useRef(null);
+
+ const item: GroupByItemInterface = {
+ dragIndex: index,
+ type: GroupByItemType,
+ };
+ const [, drag] = useDrag({
+ item,
+ collect: (monitor: DragSourceMonitor) => ({
+ isDragging: monitor.isDragging(),
+ }),
+ });
+
+ const [, drop] = useDrop({
+ accept: GroupByItemType,
+
+ hover: (item: GroupByItemInterface, monitor: DropTargetMonitor) => {
+ if (!ref.current) {
+ return;
+ }
+ const { dragIndex } = item;
+ const hoverIndex = index;
+
+ // Don't replace items with themselves
+ if (dragIndex === hoverIndex) {
+ return;
+ }
+ // Determine rectangle on screen
+ const hoverBoundingRect = ref.current?.getBoundingClientRect();
+ // Get vertical middle
+ const hoverMiddleY =
+ (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
+ // Determine mouse position
+ const clientOffset = monitor.getClientOffset();
+ // Get pixels to the top
+ const hoverClientY = clientOffset?.y
+ ? clientOffset?.y - hoverBoundingRect.top
+ : 0;
+ // Only perform the move when the mouse has crossed half of the items height
+ // When dragging downwards, only move when the cursor is below 50%
+ // When dragging upwards, only move when the cursor is above 50%
+ // Dragging downwards
+ if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
+ return;
+ }
+ // Dragging upwards
+ if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
+ return;
+ }
+
+ // Time to actually perform the action
+ onShiftOptions(dragIndex, hoverIndex);
+ // eslint-disable-next-line no-param-reassign
+ item.dragIndex = hoverIndex;
+ },
+ });
+
+ drag(drop(ref));
+
+ return (
+
+
+
+ );
+}
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/index.ts b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/index.ts
new file mode 100644
index 000000000000..439f42449aa6
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/index.ts
@@ -0,0 +1,19 @@
+/**
+ * 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 } from './DndColumnSelectLabel';
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts
new file mode 100644
index 000000000000..450956fabc22
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/types.ts
@@ -0,0 +1,34 @@
+/**
+ * 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 { ColumnMeta } from '@superset-ui/chart-controls';
+
+export interface OptionProps {
+ column: ColumnMeta;
+ index: number;
+ clickClose: (index: number) => void;
+ onShiftOptions: (dragIndex: number, hoverIndex: number) => void;
+ withCaret?: boolean;
+}
+
+export const GroupByItemType = 'groupByItem';
+
+export interface GroupByItemInterface {
+ type: typeof GroupByItemType;
+ dragIndex: number;
+}
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/utils/index.ts b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/utils/index.ts
new file mode 100644
index 000000000000..7a11e06f4e30
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/utils/index.ts
@@ -0,0 +1,19 @@
+/**
+ * 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 * from './optionSelector';
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/utils/optionSelector.ts b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/utils/optionSelector.ts
new file mode 100644
index 000000000000..eb885e6df644
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/utils/optionSelector.ts
@@ -0,0 +1,86 @@
+/**
+ * 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 { ColumnMeta } from '@superset-ui/chart-controls';
+
+export class OptionSelector {
+ groupByOptions: ColumnMeta[];
+
+ options: { string: ColumnMeta };
+
+ isArray: boolean;
+
+ constructor(
+ options: { string: ColumnMeta },
+ values: string[] | string | null,
+ ) {
+ this.options = options;
+ let groupByValues: string[];
+ if (Array.isArray(values)) {
+ groupByValues = values;
+ this.isArray = true;
+ } else {
+ groupByValues = values ? [values] : [];
+ this.isArray = false;
+ }
+ this.groupByOptions = groupByValues
+ .map(value => {
+ if (value in options) {
+ return options[value];
+ }
+ return null;
+ })
+ .filter(Boolean);
+ }
+
+ add(value: string) {
+ if (value in this.options) {
+ this.groupByOptions.push(this.options[value]);
+ }
+ }
+
+ del(idx: number) {
+ this.groupByOptions.splice(idx, 1);
+ }
+
+ replace(idx: number, value: string) {
+ if (this.groupByOptions[idx]) {
+ this.groupByOptions[idx] = this.options[value];
+ }
+ }
+
+ swap(a: number, b: number) {
+ [this.groupByOptions[a], this.groupByOptions[b]] = [
+ this.groupByOptions[b],
+ this.groupByOptions[a],
+ ];
+ }
+
+ has(groupBy: string): boolean {
+ return !!this.getValues()?.includes(groupBy);
+ }
+
+ getValues(): string[] | string | null {
+ if (!this.isArray) {
+ return this.groupByOptions.length > 0
+ ? this.groupByOptions[0].column_name
+ : null;
+ }
+ return this.groupByOptions.map(option => option.column_name);
+ }
+}
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl.jsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl.jsx
index fd2ea23a6377..9ae816a5a82b 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl.jsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl.jsx
@@ -270,7 +270,7 @@ class AdhocFilterControl extends React.Component {
optionsForSelect(props) {
const options = [
...props.columns,
- ...[...(props.formData.metrics || []), props.formData.metric].map(
+ ...[...(props.formData?.metrics || []), props.formData?.metric].map(
metric =>
metric &&
(typeof metric === 'string'
diff --git a/superset-frontend/src/explore/components/controls/index.js b/superset-frontend/src/explore/components/controls/index.js
index ba82c3c2303a..aa86e08cf3b2 100644
--- a/superset-frontend/src/explore/components/controls/index.js
+++ b/superset-frontend/src/explore/components/controls/index.js
@@ -39,6 +39,7 @@ import VizTypeControl from './VizTypeControl';
import MetricsControl from './MetricControl/MetricsControl';
import AdhocFilterControl from './FilterControl/AdhocFilterControl';
import FilterBoxItemControl from './FilterBoxItemControl';
+import DndColumnSelectControl from './DndColumnSelectControl';
const controlMap = {
AnnotationLayerControl,
@@ -50,6 +51,7 @@ const controlMap = {
ColorSchemeControl,
DatasourceControl,
DateFilterControl,
+ DndColumnSelectControl,
FixedOrMetricControl,
HiddenControl,
SelectAsyncControl,
diff --git a/superset-frontend/src/featureFlags.ts b/superset-frontend/src/featureFlags.ts
index a7bd2cac6b7e..7cf3c1a09cb0 100644
--- a/superset-frontend/src/featureFlags.ts
+++ b/superset-frontend/src/featureFlags.ts
@@ -40,6 +40,7 @@ export enum FeatureFlag {
VERSIONED_EXPORT = 'VERSIONED_EXPORT',
GLOBAL_ASYNC_QUERIES = 'GLOBAL_ASYNC_QUERIES',
ENABLE_TEMPLATE_PROCESSING = 'ENABLE_TEMPLATE_PROCESSING',
+ ENABLE_EXPLORE_DRAG_AND_DROP = 'ENABLE_EXPLORE_DRAG_AND_DROP',
}
export type FeatureFlagMap = {
diff --git a/superset/config.py b/superset/config.py
index 8e3f395d60d8..622ff056bc63 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -345,6 +345,7 @@ def _try_json_readsha( # pylint: disable=unused-argument
# Enable experimental feature to search for other dashboards
"OMNIBAR": False,
"DASHBOARD_RBAC": False,
+ "ENABLE_EXPLORE_DRAG_AND_DROP": False,
}
# Set the default view to card/grid view if thumbnail support is enabled.