Skip to content
Snippets Groups Projects
Unverified Commit b8085f3f authored by Nick Fitzpatrick's avatar Nick Fitzpatrick Committed by GitHub
Browse files

24840 reordering funnel charts (#24948)

parent 9084a352
No related branches found
No related tags found
No related merge requests found
......@@ -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;
......
......@@ -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}
......
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}
/>
);
}
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;
`;
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>
);
};
......@@ -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>
......
......@@ -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 &&
`
......
......@@ -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: {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment