diff --git a/frontend/src/metabase/visualizations/components/FunnelNormal.jsx b/frontend/src/metabase/visualizations/components/FunnelNormal.jsx index 766179f678e069c100ac2c948a5d531d928f8111..c4d800f8ce715728db6572a50f566bed707254df 100644 --- a/frontend/src/metabase/visualizations/components/FunnelNormal.jsx +++ b/frontend/src/metabase/visualizations/components/FunnelNormal.jsx @@ -26,7 +26,11 @@ export default class FunnelNormal extends Component { const dimensionIndex = 0; const metricIndex = 1; const cols = series[0].data.cols; - const rows = series.map(s => s.data.rows[0]); + const rows = settings["funnel.rows"] + ? settings["funnel.rows"] + .filter(fr => fr.enabled) + .map(fr => series[fr.rowIndex].data.rows[0]) + : series.map(s => s.data.rows[0]); const isNarrow = gridSize && gridSize.width < 7; const isShort = gridSize && gridSize.height <= 5; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedColumns.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedColumns.jsx index acfa6f174577a077bee89656838e6adad4ad6063..9cda8046034d3835956e045dcd93043562cfecde 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedColumns.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedColumns.jsx @@ -3,45 +3,13 @@ import React, { Component } from "react"; import { t } from "ttag"; import _ from "underscore"; -import { - SortableContainer, - SortableElement, -} from "metabase/components/sortable"; import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery"; import { keyForColumn, findColumnForColumnSetting } from "metabase/lib/dataset"; import { getFriendlyName } from "metabase/visualizations/lib/utils"; import ColumnItem from "./ColumnItem"; -const SortableColumn = SortableElement( - ({ columnSetting, getColumnName, onEdit, onRemove }) => ( - <ColumnItem - title={getColumnName(columnSetting)} - onEdit={onEdit ? () => onEdit(columnSetting) : null} - onRemove={onRemove ? () => onRemove(columnSetting) : null} - draggable - /> - ), -); - -const SortableColumnList = SortableContainer( - ({ columnSettings, getColumnName, onEdit, onRemove }) => { - return ( - <div> - {columnSettings.map((columnSetting, index) => ( - <SortableColumn - key={`item-${index}`} - index={columnSetting.index} - columnSetting={columnSetting} - getColumnName={getColumnName} - onEdit={onEdit} - onRemove={onRemove} - /> - ))} - </div> - ); - }, -); +import { ChartSettingOrderedItems } from "./ChartSettingOrderedItems"; export default class ChartSettingOrderedColumns extends Component { handleEnable = columnSetting => { @@ -117,9 +85,9 @@ export default class ChartSettingOrderedColumns extends Component { return ( <div className="list"> {enabledColumns.length > 0 ? ( - <SortableColumnList - columnSettings={enabledColumns} - getColumnName={this.getColumnName} + <ChartSettingOrderedItems + items={enabledColumns} + getItemName={this.getColumnName} onEdit={this.handleEdit} onRemove={this.handleDisable} onSortEnd={this.handleSortEnd} diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedItems.tsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedItems.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0089a30cd531cf753fad7e58e5501cb75bfb0ead --- /dev/null +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedItems.tsx @@ -0,0 +1,125 @@ +import React, { ReactElement } from "react"; + +import { + SortableContainer, + SortableElement, +} from "metabase/components/sortable"; + +import type { SortableElementProps } from "react-sortable-hoc"; + +import ColumnItem from "./ColumnItem"; + +interface SortableItem { + enabled: boolean; +} + +interface SortableColumnFunctions<T> { + onRemove?: (item: T) => void; + onEdit?: (item: T) => void; + onClick?: (item: T) => void; + onAdd?: (item: T) => void; + onEnable?: (item: T) => void; + getItemName: (item: T) => string; +} + +interface SortableColumnProps<T> extends SortableColumnFunctions<T> { + item: T; +} + +const SortableColumn = SortableElement(function SortableColumn< + T extends SortableItem, +>({ + item, + getItemName, + onEdit, + onRemove, + onClick, + onAdd, + onEnable, +}: SortableColumnProps<T>) { + return ( + <ColumnItem + title={getItemName(item)} + onEdit={onEdit ? () => onEdit(item) : null} + onRemove={onRemove && item.enabled ? () => onRemove(item) : null} + onClick={onClick ? () => onClick(item) : null} + onAdd={onAdd ? () => onAdd(item) : null} + onEnable={onEnable && !item.enabled ? () => onEnable(item) : null} + draggable + /> + ); +}) as unknown as <T extends SortableItem>( + props: SortableColumnProps<T> & SortableElementProps, +) => ReactElement; + +interface SortableColumnListProps<T extends SortableItem> + extends SortableColumnFunctions<T> { + items: T[]; +} + +const SortableColumnList = SortableContainer(function SortableColumnList< + T extends SortableItem, +>({ + items, + getItemName, + onEdit, + onRemove, + onEnable, + onAdd, +}: SortableColumnListProps<T>) { + return ( + <div> + {items.map((item, index: number) => ( + <SortableColumn + key={`item-${index}`} + index={index} + item={item} + getItemName={getItemName} + onEdit={onEdit} + onRemove={onRemove} + onEnable={onEnable} + onAdd={onAdd} + /> + ))} + </div> + ); +}); + +interface ChartSettingOrderedItemsProps<T extends SortableItem> + extends SortableColumnFunctions<T> { + onSortEnd: ({ + oldIndex, + newIndex, + }: { + oldIndex: number; + newIndex: number; + }) => void; + items: T[]; + distance: number; +} + +export function ChartSettingOrderedItems<T extends SortableItem>({ + onRemove, + onSortEnd, + onEdit, + onAdd, + onEnable, + onClick, + getItemName, + items, +}: ChartSettingOrderedItemsProps<T>) { + return ( + <SortableColumnList + helperClass="dragging" + items={items} + getItemName={getItemName} + onEdit={onEdit} + onRemove={onRemove} + onAdd={onAdd} + onEnable={onEnable} + onClick={onClick} + onSortEnd={onSortEnd} + distance={5} + /> + ); +} diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedRows.styled.tsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedRows.styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a3ba39815dc0034b126cc6655a7b2ae756b30ee5 --- /dev/null +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedRows.styled.tsx @@ -0,0 +1,18 @@ +import styled from "@emotion/styled"; +import { color } from "metabase/lib/colors"; + +export const ChartSettingOrderedRowsRoot = styled.div` + margin-left: 0.5rem; +`; + +export const ChartSettingMessage = styled.div` + margin: 1rem 0; + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; + background: ${color("bg-light")}; + color: ${color("text-light")}; + font-weight: 700; + border-radius: 0.5rem; +`; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedRows.tsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedRows.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f572a148b27af6a8902ac544f91ad2139926922a --- /dev/null +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedRows.tsx @@ -0,0 +1,71 @@ +import { updateIn } from "icepick"; +import React from "react"; +import { t } from "ttag"; + +import { ChartSettingOrderedItems } from "./ChartSettingOrderedItems"; + +import { + ChartSettingMessage, + ChartSettingOrderedRowsRoot, +} from "./ChartSettingOrderedRows.styled"; + +interface Row { + enabled: boolean; + rowIndex: number; + name: string; +} + +interface ChartSettingOrderedRowsProps { + onChange: (rows: Row[]) => void; + rows: Row[]; + value: Row[]; +} + +export const ChartSettingOrderedRows = ({ + onChange, + rows, + value: orderedRows, +}: ChartSettingOrderedRowsProps) => { + const handleDisable = (row: Row) => { + const index = orderedRows.findIndex(r => r.rowIndex === row.rowIndex); + onChange( + updateIn(orderedRows, [index], row => ({ + ...row, + enabled: !row.enabled, + })), + ); + }; + + const handleSortEnd = ({ + oldIndex, + newIndex, + }: { + oldIndex: number; + newIndex: number; + }) => { + const rowsCopy = [...orderedRows]; + rowsCopy.splice(newIndex, 0, rowsCopy.splice(oldIndex, 1)[0]); + onChange(rowsCopy); + }; + + const getRowTitle = (row: Row) => { + return rows.find(r => r.rowIndex === row.rowIndex)?.name || "Unknown"; + }; + + return ( + <ChartSettingOrderedRowsRoot> + {orderedRows.length > 0 ? ( + <ChartSettingOrderedItems + items={orderedRows} + getItemName={getRowTitle} + onRemove={handleDisable} + onEnable={handleDisable} + onSortEnd={handleSortEnd} + distance={5} + /> + ) : ( + <ChartSettingMessage>{t`Nothing to order`}</ChartSettingMessage> + )} + </ChartSettingOrderedRowsRoot> + ); +}; diff --git a/frontend/src/metabase/visualizations/components/settings/ColumnItem.jsx b/frontend/src/metabase/visualizations/components/settings/ColumnItem.jsx index 2dd9d42414486fdee36a74aad95b3c8403d9191f..448d7d9b1036fed66dee373e64f8b3994e136126 100644 --- a/frontend/src/metabase/visualizations/components/settings/ColumnItem.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ColumnItem.jsx @@ -20,7 +20,15 @@ const ActionIcon = ({ icon, onClick }) => ( /> ); -const ColumnItem = ({ title, onAdd, onRemove, onClick, onEdit, draggable }) => ( +const ColumnItem = ({ + title, + onAdd, + onRemove, + onClick, + onEdit, + onEnable, + draggable, +}) => ( <ColumnItemRoot draggable={draggable} onClick={onClick}> <ColumnItemContainer> {draggable && <ColumnItemDragHandle name="grabber2" />} @@ -29,6 +37,7 @@ const ColumnItem = ({ title, onAdd, onRemove, onClick, onEdit, draggable }) => ( {onEdit && <ActionIcon icon="ellipsis" onClick={onEdit} />} {onAdd && <ActionIcon icon="add" onClick={onAdd} />} {onRemove && <ActionIcon icon="eye_filled" onClick={onRemove} />} + {onEnable && <ActionIcon icon="eye_crossed_out" onClick={onEnable} />} </ColumnItemContent> </ColumnItemContainer> </ColumnItemRoot> diff --git a/frontend/src/metabase/visualizations/components/settings/ColumnItem.styled.tsx b/frontend/src/metabase/visualizations/components/settings/ColumnItem.styled.tsx index 693db2ecdd590dcea1858e39b54c767483b7db61..46579b6a2a34366e8bf1216a7d97dde4bf210b0f 100644 --- a/frontend/src/metabase/visualizations/components/settings/ColumnItem.styled.tsx +++ b/frontend/src/metabase/visualizations/components/settings/ColumnItem.styled.tsx @@ -12,6 +12,11 @@ export const ColumnItemRoot = styled.div` border-radius: 0.5rem; background: ${color("white")}; + &.dragging { + cursor: grabbing; + pointer-events: auto !important; + } + ${props => props.draggable && ` diff --git a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx index c7c5d7e01e18d83844d65009494ab2c950e4be5b..661fd58fc6c6fbc3e0de04ec39b56523401f2e82 100644 --- a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx +++ b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx @@ -26,6 +26,7 @@ import _ from "underscore"; import cx from "classnames"; import ChartCaption from "metabase/visualizations/components/ChartCaption"; +import { ChartSettingOrderedRows } from "metabase/visualizations/components/settings/ChartSettingOrderedRows"; const propTypes = { headerIcon: PropTypes.shape(iconPropTypes), @@ -109,6 +110,36 @@ export default class Funnel extends Component { useRawSeries: true, showColumnSetting: true, }), + "funnel.rows": { + section: t`Data`, + widget: ChartSettingOrderedRows, + isValid: (series, settings) => { + const funnelRows = settings["funnel.rows"]; + + if (!funnelRows || !_.isArray(funnelRows)) { + return false; + } + if (!funnelRows.every(setting => setting.rowIndex !== undefined)) { + return false; + } + + return ( + funnelRows.every(setting => series[setting.rowIndex]) && + funnelRows.length === series.length + ); + }, + + getDefault: transformedSeries => { + return transformedSeries.map(s => ({ + name: s.card.name, + rowIndex: s.card.rowIndex, + enabled: true, + })); + }, + getProps: transformedSeries => ({ + rows: transformedSeries.map(s => s.card), + }), + }, ...metricSetting("funnel.metric", { section: t`Data`, title: t`Measure`, @@ -158,12 +189,13 @@ export default class Funnel extends Component { dimensionIndex >= 0 && metricIndex >= 0 ) { - return rows.map(row => ({ + return rows.map((row, index) => ({ card: { ...card, name: formatValue(row[dimensionIndex], { column: cols[dimensionIndex], }), + rowIndex: index, _transformed: true, }, data: {