From ed7e147b7cdace92b6f84e35523558927d1f5821 Mon Sep 17 00:00:00 2001 From: Alexander Polyankin <alexander.polyankin@metabase.com> Date: Thu, 14 Mar 2024 18:06:59 +0200 Subject: [PATCH] Add UI to swap clauses in the notebook (#39205) (#40131) --- .../helpers/e2e-ui-elements-helpers.js | 11 +- .../visualization-options.cy.spec.js | 6 +- .../scenarios/question/notebook.cy.spec.js | 63 ++++++++ .../scenarios/question/settings.cy.spec.js | 10 +- .../bar_chart.cy.spec.js | 6 +- .../visualizations-charts/funnel.cy.spec.js | 6 +- .../28311-sorting-table-columns.cy.spec.js | 9 +- .../visualizations-tabular/table.cy.spec.js | 9 +- frontend/src/metabase-lib/query.ts | 9 ++ frontend/src/metabase-lib/types.ts | 2 + .../steps/AggregateStep/AggregateStep.tsx | 18 ++- .../steps/BreakoutStep/BreakoutStep.tsx | 14 ++ .../notebook/steps/ClauseStep/ClauseStep.tsx | 147 ++++++++++++++---- .../notebook/steps/ExpressionStep.tsx | 29 +++- .../notebook/steps/FilterStep/FilterStep.tsx | 42 +++-- .../notebook/steps/SortStep/SortStep.tsx | 14 ++ .../components/FilterPicker/FilterPicker.tsx | 8 +- 17 files changed, 320 insertions(+), 83 deletions(-) diff --git a/e2e/support/helpers/e2e-ui-elements-helpers.js b/e2e/support/helpers/e2e-ui-elements-helpers.js index 5bf030939c9..5e64e90e2e7 100644 --- a/e2e/support/helpers/e2e-ui-elements-helpers.js +++ b/e2e/support/helpers/e2e-ui-elements-helpers.js @@ -168,8 +168,11 @@ export const moveColumnDown = (column, distance) => { .trigger("mouseup", 0, distance * 50, { force: true }); }; -export const moveDnDKitColumnVertical = (column, distance) => { - column +export const moveDnDKitElement = ( + element, + { horizontal = 0, vertical = 0 } = {}, +) => { + element .trigger("pointerdown", 0, 0, { force: true, isPrimary: true, @@ -182,13 +185,13 @@ export const moveDnDKitColumnVertical = (column, distance) => { button: 0, }) .wait(200) - .trigger("pointermove", 0, distance, { + .trigger("pointermove", horizontal, vertical, { force: true, isPrimary: true, button: 0, }) .wait(200) - .trigger("pointerup", 0, distance, { + .trigger("pointerup", horizontal, vertical, { force: true, isPrimary: true, button: 0, diff --git a/e2e/test/scenarios/dashboard-cards/visualization-options.cy.spec.js b/e2e/test/scenarios/dashboard-cards/visualization-options.cy.spec.js index 84fb51696cf..0c34444e004 100644 --- a/e2e/test/scenarios/dashboard-cards/visualization-options.cy.spec.js +++ b/e2e/test/scenarios/dashboard-cards/visualization-options.cy.spec.js @@ -10,7 +10,7 @@ import { saveDashboard, getDashboardCardMenu, getDraggableElements, - moveDnDKitColumnVertical, + moveDnDKitElement, } from "e2e/support/helpers"; describe("scenarios > dashboard cards > visualization options", () => { @@ -51,7 +51,9 @@ describe("scenarios > dashboard cards > visualization options", () => { getDashboardCard().realHover(); cy.findByLabelText("Show visualization options").click(); cy.findByTestId("chartsettings-sidebar").within(() => { - moveDnDKitColumnVertical(getDraggableElements().contains("ID"), 100); + moveDnDKitElement(getDraggableElements().contains("ID"), { + vertical: 100, + }); /** * When this issue gets fixed, it should be safe to uncomment the following assertion. diff --git a/e2e/test/scenarios/question/notebook.cy.spec.js b/e2e/test/scenarios/question/notebook.cy.spec.js index e45655ab063..ad34cc591ae 100644 --- a/e2e/test/scenarios/question/notebook.cy.spec.js +++ b/e2e/test/scenarios/question/notebook.cy.spec.js @@ -10,6 +10,7 @@ import { getNotebookStep, hovercard, join, + moveDnDKitElement, openNotebook, openOrdersTable, openProductsTable, @@ -708,6 +709,68 @@ describe("scenarios > question > notebook", { tags: "@slow" }, () => { assertTableRowCount(10); }); }); + + it("should be able to drag-n-drop query clauses", () => { + function moveElement({ name, horizontal, vertical, index }) { + moveDnDKitElement(cy.findByText(name), { + horizontal, + vertical, + }); + cy.findAllByTestId("notebook-cell-item") + .eq(index) + .should("have.text", name); + } + + const questionDetails = { + query: { + "source-table": ORDERS_ID, + expressions: { + E1: ["+", ["field", ORDERS.ID, null], 1], + E2: ["+", ["field", ORDERS.ID, null], 2], + }, + filter: [ + "and", + ["=", ["field", ORDERS.ID, null], 1], + ["=", ["field", ORDERS.ID, null], 2], + ["=", ["field", ORDERS.ID, null], 3], + ], + breakout: [ + ["field", ORDERS.ID, null], + ["field", ORDERS.PRODUCT_ID, null], + ], + aggregation: [ + ["count"], + ["sum", ["field", ORDERS.TAX, null]], + ["sum", ["field", ORDERS.SUBTOTAL, null]], + ["sum", ["field", ORDERS.TOTAL, null]], + ["avg", ["field", ORDERS.TOTAL, null]], + ], + "order-by": [ + ["asc", ["aggregation", 0]], + ["asc", ["aggregation", 4]], + ], + }, + }; + cy.createQuestion(questionDetails, { visitQuestion: true }); + openNotebook(); + getNotebookStep("expression").within(() => { + moveElement({ name: "E1", horizontal: 100, index: 1 }); + }); + getNotebookStep("filter").within(() => { + moveElement({ name: "ID is 2", horizontal: -100, index: 0 }); + }); + getNotebookStep("summarize").within(() => { + cy.findByTestId("aggregate-step").within(() => { + moveElement({ name: "Count", vertical: 100, index: 3 }); + }); + cy.findByTestId("breakout-step").within(() => { + moveElement({ name: "ID", horizontal: 100, index: 1 }); + }); + }); + getNotebookStep("sort").within(() => { + moveElement({ name: "Average of Total", horizontal: -100, index: 0 }); + }); + }); }); function assertTableRowCount(expectedCount) { diff --git a/e2e/test/scenarios/question/settings.cy.spec.js b/e2e/test/scenarios/question/settings.cy.spec.js index 8d42d46a7e2..e304da8ca7e 100644 --- a/e2e/test/scenarios/question/settings.cy.spec.js +++ b/e2e/test/scenarios/question/settings.cy.spec.js @@ -9,7 +9,7 @@ import { popover, modal, sidebar, - moveDnDKitColumnVertical, + moveDnDKitElement, } from "e2e/support/helpers"; const { ORDERS, ORDERS_ID, PRODUCTS, PRODUCTS_ID } = SAMPLE_DATABASE; @@ -100,7 +100,7 @@ describe("scenarios > question > settings", () => { getSidebarColumns().eq("5").as("total").contains("Total"); - moveDnDKitColumnVertical(cy.get("@total"), -100); + moveDnDKitElement(cy.get("@total"), { vertical: -100 }); getSidebarColumns().eq("3").should("contain.text", "Total"); @@ -114,7 +114,7 @@ describe("scenarios > question > settings", () => { expect($el.scrollTop).to.eql(0); }); - moveDnDKitColumnVertical(cy.get("@title"), 15); + moveDnDKitElement(cy.get("@title"), { vertical: 15 }); cy.findByTestId("chartsettings-sidebar").should(([$el]) => { expect($el.scrollTop).to.be.greaterThan(0); @@ -156,7 +156,7 @@ describe("scenarios > question > settings", () => { .contains(/Products? → Category/); // Drag and drop this column between "Tax" and "Discount" (index 5 in @sidebarColumns array) - moveDnDKitColumnVertical(cy.get("@prod-category"), -360); + moveDnDKitElement(cy.get("@prod-category"), { vertical: -360 }); refreshResultsInHeader(); @@ -187,7 +187,7 @@ describe("scenarios > question > settings", () => { findColumnAtIndex("User → Address", -1).as("user-address"); // Move it one place up - moveDnDKitColumnVertical(cy.get("@user-address"), -100); + moveDnDKitElement(cy.get("@user-address"), { vertical: -100 }); findColumnAtIndex("User → Address", -3); diff --git a/e2e/test/scenarios/visualizations-charts/bar_chart.cy.spec.js b/e2e/test/scenarios/visualizations-charts/bar_chart.cy.spec.js index a40971b9b51..f910df99af5 100644 --- a/e2e/test/scenarios/visualizations-charts/bar_chart.cy.spec.js +++ b/e2e/test/scenarios/visualizations-charts/bar_chart.cy.spec.js @@ -8,7 +8,7 @@ import { popover, visitDashboard, cypressWaitAll, - moveDnDKitColumnVertical, + moveDnDKitElement, } from "e2e/support/helpers"; const { ORDERS, ORDERS_ID, PEOPLE, PRODUCTS, PRODUCTS_ID } = SAMPLE_DATABASE; @@ -143,7 +143,7 @@ describe("scenarios > visualizations > bar chart", () => { }); it("should allow you to show/hide and reorder columns", () => { - moveDnDKitColumnVertical(getDraggableElements().eq(0), 100); + moveDnDKitElement(getDraggableElements().eq(0), { vertical: 100 }); cy.findAllByTestId("legend-item").eq(0).should("contain.text", "Gadget"); cy.findAllByTestId("legend-item").eq(1).should("contain.text", "Gizmo"); @@ -192,7 +192,7 @@ describe("scenarios > visualizations > bar chart", () => { }); it("should gracefully handle removing filtered items, and adding new items to the end of the list", () => { - moveDnDKitColumnVertical(getDraggableElements().first(), 100); + moveDnDKitElement(getDraggableElements().first(), { vertical: 100 }); getDraggableElements() .eq(1) diff --git a/e2e/test/scenarios/visualizations-charts/funnel.cy.spec.js b/e2e/test/scenarios/visualizations-charts/funnel.cy.spec.js index 22d20569a3a..b35c7ff2f84 100644 --- a/e2e/test/scenarios/visualizations-charts/funnel.cy.spec.js +++ b/e2e/test/scenarios/visualizations-charts/funnel.cy.spec.js @@ -6,7 +6,7 @@ import { sidebar, getDraggableElements, popover, - moveDnDKitColumnVertical, + moveDnDKitElement, } from "e2e/support/helpers"; const { PEOPLE_ID, PEOPLE } = SAMPLE_DATABASE; @@ -45,7 +45,7 @@ describe("scenarios > visualizations > funnel chart", () => { .first() .should("have.text", name); - moveDnDKitColumnVertical(getDraggableElements().first(), 100); + moveDnDKitElement(getDraggableElements().first(), { vertical: 100 }); getDraggableElements().eq(2).should("have.text", name); @@ -71,7 +71,7 @@ describe("scenarios > visualizations > funnel chart", () => { }); it("should handle row items being filterd out and returned gracefully", () => { - moveDnDKitColumnVertical(getDraggableElements().first(), 100); + moveDnDKitElement(getDraggableElements().first(), { vertical: 100 }); getDraggableElements() .eq(1) diff --git a/e2e/test/scenarios/visualizations-tabular/reproductions/28311-sorting-table-columns.cy.spec.js b/e2e/test/scenarios/visualizations-tabular/reproductions/28311-sorting-table-columns.cy.spec.js index a643221a07f..d693a599c45 100644 --- a/e2e/test/scenarios/visualizations-tabular/reproductions/28311-sorting-table-columns.cy.spec.js +++ b/e2e/test/scenarios/visualizations-tabular/reproductions/28311-sorting-table-columns.cy.spec.js @@ -4,7 +4,7 @@ import { restore, visitQuestionAdhoc, getDraggableElements, - moveDnDKitColumnVertical, + moveDnDKitElement, } from "e2e/support/helpers"; const { ORDERS_ID, ORDERS } = SAMPLE_DATABASE; @@ -62,10 +62,9 @@ describe("issue 25250", () => { cy.findByText("Product ID").should("be.visible"); cy.findByTestId("viz-settings-button").click(); - moveDnDKitColumnVertical( - getDraggableElements().contains("Product ID"), - -100, - ); + moveDnDKitElement(getDraggableElements().contains("Product ID"), { + vertical: -100, + }); getDraggableElements().eq(0).should("contain", "Product ID"); }); }); diff --git a/e2e/test/scenarios/visualizations-tabular/table.cy.spec.js b/e2e/test/scenarios/visualizations-tabular/table.cy.spec.js index 09b73e718d1..702bb7442c6 100644 --- a/e2e/test/scenarios/visualizations-tabular/table.cy.spec.js +++ b/e2e/test/scenarios/visualizations-tabular/table.cy.spec.js @@ -17,7 +17,7 @@ import { getTable, leftSidebar, sidebar, - moveDnDKitColumnVertical, + moveDnDKitElement, } from "e2e/support/helpers"; describe("scenarios > visualizations > table", () => { @@ -431,10 +431,9 @@ describe("scenarios > visualizations > table > conditional formatting", () => { .first() .should("contain.text", "is less than 20"); - moveDnDKitColumnVertical( - cy.findAllByTestId("formatting-rule-preview").eq(2), - -300, - ); + moveDnDKitElement(cy.findAllByTestId("formatting-rule-preview").eq(2), { + vertical: -300, + }); cy.findAllByTestId("formatting-rule-preview") .first() diff --git a/frontend/src/metabase-lib/query.ts b/frontend/src/metabase-lib/query.ts index 0b6e2907060..d069ff74ee1 100644 --- a/frontend/src/metabase-lib/query.ts +++ b/frontend/src/metabase-lib/query.ts @@ -81,6 +81,15 @@ export function replaceClause( return ML.replace_clause(query, stageIndex, targetClause, newClause); } +export function swapClauses( + query: Query, + stageIndex: number, + sourceClause: Clause, + targetClause: Clause, +): Query { + return ML.swap_clauses(query, stageIndex, sourceClause, targetClause); +} + export function sourceTableOrCardId(query: Query): TableId | null { return ML.source_table_or_card_id(query); } diff --git a/frontend/src/metabase-lib/types.ts b/frontend/src/metabase-lib/types.ts index f7e1c57b43b..f9735278cf3 100644 --- a/frontend/src/metabase-lib/types.ts +++ b/frontend/src/metabase-lib/types.ts @@ -65,6 +65,8 @@ export type OrderByDirection = "asc" | "desc"; declare const FilterClause: unique symbol; export type FilterClause = unknown & { _opaque: typeof FilterClause }; +export type Filterable = FilterClause | ExpressionClause | SegmentMetadata; + declare const Join: unique symbol; export type Join = unknown & { _opaque: typeof Join }; diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/AggregateStep/AggregateStep.tsx b/frontend/src/metabase/query_builder/components/notebook/steps/AggregateStep/AggregateStep.tsx index 039fcd7edcd..86cf095a3bd 100644 --- a/frontend/src/metabase/query_builder/components/notebook/steps/AggregateStep/AggregateStep.tsx +++ b/frontend/src/metabase/query_builder/components/notebook/steps/AggregateStep/AggregateStep.tsx @@ -39,8 +39,21 @@ export function AggregateStep({ updateQuery(nextQuery); }; - const handleRemoveAggregation = (aggregation: Lib.AggregationClause) => { - const nextQuery = Lib.removeClause(query, stageIndex, aggregation); + const handleReorderAggregation = ( + sourceClause: Lib.AggregationClause, + targetClause: Lib.AggregationClause, + ) => { + const nextQuery = Lib.swapClauses( + query, + stageIndex, + sourceClause, + targetClause, + ); + updateQuery(nextQuery); + }; + + const handleRemoveAggregation = (clause: Lib.AggregationClause) => { + const nextQuery = Lib.removeClause(query, stageIndex, clause); updateQuery(nextQuery); }; @@ -66,6 +79,7 @@ export function AggregateStep({ onClose={onClose} /> )} + onReorder={handleReorderAggregation} onRemove={handleRemoveAggregation} data-testid="aggregate-step" /> diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/BreakoutStep/BreakoutStep.tsx b/frontend/src/metabase/query_builder/components/notebook/steps/BreakoutStep/BreakoutStep.tsx index 83b42d1343b..d4fd33c4da9 100644 --- a/frontend/src/metabase/query_builder/components/notebook/steps/BreakoutStep/BreakoutStep.tsx +++ b/frontend/src/metabase/query_builder/components/notebook/steps/BreakoutStep/BreakoutStep.tsx @@ -37,6 +37,19 @@ function BreakoutStep({ updateQuery(nextQuery); }; + const handleReorderBreakout = ( + sourceClause: Lib.BreakoutClause, + targetClause: Lib.BreakoutClause, + ) => { + const nextQuery = Lib.swapClauses( + query, + stageIndex, + sourceClause, + targetClause, + ); + updateQuery(nextQuery); + }; + const handleRemoveBreakout = (clause: Lib.BreakoutClause) => { const nextQuery = Lib.removeClause(query, stageIndex, clause); updateQuery(nextQuery); @@ -61,6 +74,7 @@ function BreakoutStep({ onClose={onClose} /> )} + onReorder={handleReorderBreakout} onRemove={handleRemoveBreakout} data-testid="breakout-step" /> diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/ClauseStep/ClauseStep.tsx b/frontend/src/metabase/query_builder/components/notebook/steps/ClauseStep/ClauseStep.tsx index ea6113706cb..48ee0a0c7e6 100644 --- a/frontend/src/metabase/query_builder/components/notebook/steps/ClauseStep/ClauseStep.tsx +++ b/frontend/src/metabase/query_builder/components/notebook/steps/ClauseStep/ClauseStep.tsx @@ -1,3 +1,11 @@ +import type { DndContextProps } from "@dnd-kit/core"; +import { PointerSensor, useSensor, DndContext } from "@dnd-kit/core"; +import { restrictToParentElement } from "@dnd-kit/modifiers"; +import { SortableContext, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import type { ReactNode } from "react"; +import { useCallback } from "react"; + import { Icon } from "metabase/ui"; import { @@ -20,43 +28,35 @@ type RenderPopoverOpts<T> = { onClose: () => void; }; -export interface ClauseStepProps<T> { +export type ClauseStepProps<T> = { color: string; items: T[]; - isLastOpened?: boolean; - initialAddText?: string | null; + initialAddText?: string; readOnly?: boolean; + isLastOpened?: boolean; renderName: (item: T, index: number) => JSX.Element | string; renderPopover: (opts: RenderPopoverOpts<T>) => JSX.Element | null; - canRemove?: (item: T) => boolean; - onRemove?: ((item: T, index: number) => void) | null; + onRemove: (item: T, index: number) => void; + onReorder: (sourceItem: T, targetItem: T) => void; "data-testid"?: string; -} +}; export const ClauseStep = <T,>({ color, items, + initialAddText, + readOnly = false, isLastOpened = false, - initialAddText = null, - readOnly, renderName, renderPopover, - canRemove, - onRemove = null, + onRemove, + onReorder, ...props }: ClauseStepProps<T>): JSX.Element => { - const renderNewItem = ({ onOpen }: { onOpen?: () => void }) => ( - <NotebookCellAdd - initialAddText={items.length === 0 && initialAddText} - color={color} - onClick={onOpen} - /> - ); - const renderItem = ({ item, index, onOpen }: RenderItemOpts<T>) => ( <NotebookCellItem color={color} readOnly={readOnly} onClick={onOpen}> {renderName(item, index)} - {!readOnly && onRemove && (!canRemove || canRemove(item)) && ( + {!readOnly && ( <Icon className="ml1" name="close" @@ -69,15 +69,26 @@ export const ClauseStep = <T,>({ </NotebookCellItem> ); + const renderNewItem = ({ onOpen }: { onOpen?: () => void }) => ( + <NotebookCellAdd + initialAddText={items.length === 0 && initialAddText} + color={color} + onClick={onOpen} + /> + ); + return ( <NotebookCell color={color} data-testid={props["data-testid"]}> - {items.map((item, index) => ( - <ClausePopover - key={index} - renderItem={onOpen => renderItem({ item, index, onOpen })} - renderPopover={onClose => renderPopover({ item, index, onClose })} - /> - ))} + <ClauseStepDndContext items={items} onReorder={onReorder}> + {items.map((item, index) => ( + <ClauseStepDndItem key={index} index={index} readOnly={readOnly}> + <ClausePopover + renderItem={onOpen => renderItem({ item, index, onOpen })} + renderPopover={onClose => renderPopover({ item, index, onClose })} + /> + </ClauseStepDndItem> + ))} + </ClauseStepDndContext> {!readOnly && ( <ClausePopover isInitiallyOpen={isLastOpened} @@ -88,3 +99,87 @@ export const ClauseStep = <T,>({ </NotebookCell> ); }; + +type ClauseStepDndContextProps<T> = { + items: T[]; + children: ReactNode; + onReorder: (sourceItem: T, targetItem: T) => void; +}; + +function ClauseStepDndContext<T>({ + items, + children, + onReorder, +}: ClauseStepDndContextProps<T>) { + const pointerSensor = useSensor(PointerSensor, { + activationConstraint: { distance: 0 }, + }); + + const handleSortEnd: DndContextProps["onDragEnd"] = useCallback( + input => { + if (input.over) { + const sourceIndex = getItemIndexFromId(input.active.id); + const targetIndex = getItemIndexFromId(input.over.id); + onReorder(items[sourceIndex], items[targetIndex]); + } + }, + [items, onReorder], + ); + + return ( + <DndContext + sensors={[pointerSensor]} + modifiers={[restrictToParentElement]} + onDragEnd={handleSortEnd} + > + <SortableContext + items={items.map((_, index) => getItemIdFromIndex(index))} + > + {children} + </SortableContext> + </DndContext> + ); +} + +type ClauseStepDndItemProps = { + index: number; + readOnly: boolean; + children: ReactNode; +}; + +function ClauseStepDndItem({ + index, + readOnly, + children, +}: ClauseStepDndItemProps) { + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ + id: getItemIdFromIndex(index), + disabled: readOnly, + // disable animation after reordering because we don't have stable item ids + animateLayoutChanges: () => false, + }); + + return ( + <div + ref={setNodeRef} + {...attributes} + {...listeners} + style={{ + transition, + transform: CSS.Translate.toString(transform), + }} + > + {children} + </div> + ); +} + +// dnd-kit ignores `0` item, so we convert indexes to string `"0"` +function getItemIdFromIndex(index: number) { + return String(index); +} + +function getItemIndexFromId(id: string | number) { + return Number(id); +} diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/ExpressionStep.tsx b/frontend/src/metabase/query_builder/components/notebook/steps/ExpressionStep.tsx index 040ef472f95..bc16416ff1f 100644 --- a/frontend/src/metabase/query_builder/components/notebook/steps/ExpressionStep.tsx +++ b/frontend/src/metabase/query_builder/components/notebook/steps/ExpressionStep.tsx @@ -17,8 +17,27 @@ export const ExpressionStep = ({ const { query, stageIndex } = step; const expressions = Lib.expressions(query, stageIndex); - const renderExpressionName = (expression: Lib.ExpressionClause) => - Lib.displayInfo(query, stageIndex, expression).longDisplayName; + const renderExpressionName = (expression: Lib.ExpressionClause) => { + return Lib.displayInfo(query, stageIndex, expression).longDisplayName; + }; + + const handleReorderExpression = ( + sourceClause: Lib.ExpressionClause, + targetClause: Lib.ExpressionClause, + ) => { + const nextQuery = Lib.swapClauses( + query, + stageIndex, + sourceClause, + targetClause, + ); + updateQuery(nextQuery); + }; + + const handleRemoveExpression = (clause: Lib.ExpressionClause) => { + const nextQuery = Lib.removeClause(query, stageIndex, clause); + updateQuery(nextQuery); + }; return ( <ClauseStep @@ -71,10 +90,8 @@ export const ExpressionStep = ({ /> )} isLastOpened={isLastOpened} - onRemove={clause => { - const nextQuery = Lib.removeClause(query, stageIndex, clause); - updateQuery(nextQuery); - }} + onReorder={handleReorderExpression} + onRemove={handleRemoveExpression} /> ); }; diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/FilterStep/FilterStep.tsx b/frontend/src/metabase/query_builder/components/notebook/steps/FilterStep/FilterStep.tsx index fa934103a9c..c03c3c300ed 100644 --- a/frontend/src/metabase/query_builder/components/notebook/steps/FilterStep/FilterStep.tsx +++ b/frontend/src/metabase/query_builder/components/notebook/steps/FilterStep/FilterStep.tsx @@ -23,33 +23,44 @@ export function FilterStep({ [query, stageIndex], ); - const handleAddFilter = ( - filter: Lib.ExpressionClause | Lib.FilterClause | Lib.SegmentMetadata, - ) => { - const nextQuery = Lib.filter(query, stageIndex, filter); + const renderFilterName = (filter: Lib.FilterClause) => + Lib.displayInfo(query, stageIndex, filter).longDisplayName; + + const handleAddFilter = (clause: Lib.Filterable) => { + const nextQuery = Lib.filter(query, stageIndex, clause); updateQuery(nextQuery); }; const handleUpdateFilter = ( - targetFilter: Lib.FilterClause, - nextFilter: Lib.ExpressionClause | Lib.FilterClause | Lib.SegmentMetadata, + targetClause: Lib.FilterClause, + newClause: Lib.Filterable, ) => { const nextQuery = Lib.replaceClause( query, stageIndex, - targetFilter, - nextFilter, + targetClause, + newClause, ); updateQuery(nextQuery); }; - const handleRemoveFilter = (filter: Lib.FilterClause) => { - const nextQuery = Lib.removeClause(query, stageIndex, filter); + const handleReorderFilter = ( + sourceClause: Lib.FilterClause, + targetClause: Lib.FilterClause, + ) => { + const nextQuery = Lib.swapClauses( + query, + stageIndex, + sourceClause, + targetClause, + ); updateQuery(nextQuery); }; - const renderFilterName = (filter: Lib.FilterClause) => - Lib.displayInfo(query, stageIndex, filter).longDisplayName; + const handleRemoveFilter = (clause: Lib.FilterClause) => { + const nextQuery = Lib.removeClause(query, stageIndex, clause); + updateQuery(nextQuery); + }; return ( <ErrorBoundary> @@ -71,6 +82,7 @@ export function FilterStep({ onClose={onClose} /> )} + onReorder={handleReorderFilter} onRemove={handleRemoveFilter} /> </ErrorBoundary> @@ -82,12 +94,10 @@ interface FilterPopoverProps { stageIndex: number; filter?: Lib.FilterClause; filterIndex?: number; - onAddFilter: ( - filter: Lib.ExpressionClause | Lib.FilterClause | Lib.SegmentMetadata, - ) => void; + onAddFilter: (filter: Lib.Filterable) => void; onUpdateFilter: ( targetFilter: Lib.FilterClause, - nextFilter: Lib.ExpressionClause | Lib.FilterClause | Lib.SegmentMetadata, + nextFilter: Lib.Filterable, ) => void; onClose?: () => void; } diff --git a/frontend/src/metabase/query_builder/components/notebook/steps/SortStep/SortStep.tsx b/frontend/src/metabase/query_builder/components/notebook/steps/SortStep/SortStep.tsx index 3a3a27cb1b0..00edddbf935 100644 --- a/frontend/src/metabase/query_builder/components/notebook/steps/SortStep/SortStep.tsx +++ b/frontend/src/metabase/query_builder/components/notebook/steps/SortStep/SortStep.tsx @@ -43,6 +43,19 @@ function SortStep({ updateQuery(nextQuery); }; + const handleReorderOrderBy = ( + sourceClause: Lib.OrderByClause, + targetClause: Lib.OrderByClause, + ) => { + const nextQuery = Lib.swapClauses( + query, + stageIndex, + sourceClause, + targetClause, + ); + updateQuery(nextQuery); + }; + const handleRemoveOrderBy = (clause: Lib.OrderByClause) => { const nextQuery = Lib.removeClause(query, stageIndex, clause); updateQuery(nextQuery); @@ -71,6 +84,7 @@ function SortStep({ onClose={onClose} /> )} + onReorder={handleReorderOrderBy} onRemove={handleRemoveOrderBy} /> ); diff --git a/frontend/src/metabase/querying/components/FilterPicker/FilterPicker.tsx b/frontend/src/metabase/querying/components/FilterPicker/FilterPicker.tsx index bc543741d45..3372d425772 100644 --- a/frontend/src/metabase/querying/components/FilterPicker/FilterPicker.tsx +++ b/frontend/src/metabase/querying/components/FilterPicker/FilterPicker.tsx @@ -15,9 +15,7 @@ export interface FilterPickerProps { filter?: Lib.FilterClause; filterIndex?: number; - onSelect: ( - filter: Lib.ExpressionClause | Lib.FilterClause | Lib.SegmentMetadata, - ) => void; + onSelect: (filter: Lib.Filterable) => void; onClose?: () => void; } @@ -46,9 +44,7 @@ export function FilterPicker({ setFilter(initialFilter); }, [initialFilter]); - const handleChange = ( - filter: Lib.ExpressionClause | Lib.FilterClause | Lib.SegmentMetadata, - ) => { + const handleChange = (filter: Lib.Filterable) => { onSelect(filter); onClose?.(); }; -- GitLab