diff --git a/.loki/reference/chrome_laptop_Visualizations_ChartSkeleton_Pie.png b/.loki/reference/chrome_laptop_Visualizations_ChartSkeleton_Pie.png deleted file mode 100644 index 7d46a7b51a4956af00ac24716e9abb97cf6ca67c..0000000000000000000000000000000000000000 Binary files a/.loki/reference/chrome_laptop_Visualizations_ChartSkeleton_Pie.png and /dev/null differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_Default.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_Default.png index d96c5dc133a0ffc769e98d60491478872604d3cd..f9c3286235abafd947cfa98403223ab147d7835d 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_Default.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_Default.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Default.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Default.png index 49b75e6a7894feb216ac07a7f40367439085c6f4..86df491cfade4a25a4e9a8a161d8f5afd1359c88 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Default.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Default.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Scroll.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Scroll.png index 57ba1762ec20a73507e97b8fa83c9b51704ee0c2..035730051637e145167f1188db95bf064fcfd798 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Scroll.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_No_Background_Scroll.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_Scroll.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_Scroll.png index 887af107bd9239a11093ced79f466a31cad079a8..3d6052fe839d177acb38f28cd46ba7880e228a02 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_Scroll.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Dark_Theme_Scroll.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_Default.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_Default.png index 459e33d740ecca6eaa83bcdae39edf386864b950..d1def31447daa33a1bcb91f4deb3325546207ef4 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_Default.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_Default.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_No_Background_Default.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_No_Background_Default.png index 0cd7b11560662f4e7c5aceaff527635f65f35faa..9f09a27a85a1bc4ae2ab2f78628ee4a5aa30b382 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_No_Background_Default.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_No_Background_Default.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_No_Background_Scroll.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_No_Background_Scroll.png index efd35f25cd08ec6e8532dda1113550b8d3fb423b..dc470ac381d1c5a030a1d116abfc501e7a618a83 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_No_Background_Scroll.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_No_Background_Scroll.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_Scroll.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_Scroll.png index 30121b0cd223bbe1bb84e32be4d0c8bde1a8bad1..47b56fc2f3fc6b101638b813ec13280361df07dd 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_Scroll.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Light_Theme_Scroll.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_Default.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_Default.png index c13cc554dbebb977f8dad6a61dec578a93f3f9c8..6d4936e1620a62d95e660429276eec50e57ae9f1 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_Default.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_Default.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_No_Background_Default.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_No_Background_Default.png index 6e8ea38812e5ff0078b97b44e81f49e1914c8b3e..ab9f4b90262a80d48dd57557e3281cd3b9406de4 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_No_Background_Default.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_No_Background_Default.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_No_Background_Scroll.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_No_Background_Scroll.png index 84326feca1bb046004f58f7fb185f4d8ee4cd6ed..6bce2b08b50f399e91311aea2788ad725d014011 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_No_Background_Scroll.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_No_Background_Scroll.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_Scroll.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_Scroll.png index 66730a5cd353ce3ce8b45890e5ae1b1d2ca868f4..59d17da27b791707740f338b5712a7035e2decad 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_Scroll.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedDashboardView_Transparent_Theme_Scroll.png differ diff --git a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Light_Theme_Default.png b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Light_Theme_Default.png index 61b136c5c286aab977874f20bb8e1c823ce3f363..0165ae96dd3328074d4b2ab0bf1b6c079e36f139 100644 Binary files a/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Light_Theme_Default.png and b/.loki/reference/chrome_laptop_embed_PublicOrEmbeddedQuestionView_Light_Theme_Default.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ComboChart_Bar_Stack_Display_Overrides_Series_Displays.png b/.loki/reference/chrome_laptop_static_viz_ComboChart_Bar_Stack_Display_Overrides_Series_Displays.png deleted file mode 100644 index 9368bf5fbe7c461edae0a6f6eb6571c264b9f58e..0000000000000000000000000000000000000000 Binary files a/.loki/reference/chrome_laptop_static_viz_ComboChart_Bar_Stack_Display_Overrides_Series_Displays.png and /dev/null differ diff --git a/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Categorical_Line_Bar.png b/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Categorical_Line_Bar.png deleted file mode 100644 index da620b1df8bdff5026059dbfc9f85afe2f302af0..0000000000000000000000000000000000000000 Binary files a/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Categorical_Line_Bar.png and /dev/null differ diff --git a/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Line_Bar_Area.png b/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Line_Bar_Area.png deleted file mode 100644 index 33526f89eb7cc78bbef3552df85a14a1c5bde18b..0000000000000000000000000000000000000000 Binary files a/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Line_Bar_Area.png and /dev/null differ diff --git a/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Line_Two_Bars.png b/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Line_Two_Bars.png deleted file mode 100644 index 58c1f04f473146a6cad69f84a5b804bd1749a90e..0000000000000000000000000000000000000000 Binary files a/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Line_Two_Bars.png and /dev/null differ diff --git a/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Single_Series_Bar.png b/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Single_Series_Bar.png deleted file mode 100644 index 578d114b961d93c2b6f03ddc22bb920c02b12396..0000000000000000000000000000000000000000 Binary files a/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Single_Series_Bar.png and /dev/null differ diff --git a/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Single_Series_Many_Bars.png b/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Single_Series_Many_Bars.png deleted file mode 100644 index e5b1cffcec5fc9ee8df24d4100256007e2093ad0..0000000000000000000000000000000000000000 Binary files a/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Single_Series_Many_Bars.png and /dev/null differ diff --git a/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Timeseries_With_Negative_Data.png b/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Timeseries_With_Negative_Data.png deleted file mode 100644 index afb397b78af4e6891fac960334f633b310a9e685..0000000000000000000000000000000000000000 Binary files a/.loki/reference/chrome_laptop_static_viz_LineAreaBarChart_Timeseries_With_Negative_Data.png and /dev/null differ diff --git a/.loki/reference/chrome_laptop_viz_LineChart_Default.png b/.loki/reference/chrome_laptop_viz_LineChart_Default.png index b1222d607f01306a7d6a6af5af3c5ab24501e07d..703dde7af21f825c21141217610d9261f1e0dbfb 100644 Binary files a/.loki/reference/chrome_laptop_viz_LineChart_Default.png and b/.loki/reference/chrome_laptop_viz_LineChart_Default.png differ diff --git a/.loki/reference/chrome_laptop_viz_LineChart_Embedding_Huge_Font.png b/.loki/reference/chrome_laptop_viz_LineChart_Embedding_Huge_Font.png index ba0678a5625ea3665a71e5049e8d9cbc930881ce..43e3e28ec99a768099d46048cb14813168a31c35 100644 Binary files a/.loki/reference/chrome_laptop_viz_LineChart_Embedding_Huge_Font.png and b/.loki/reference/chrome_laptop_viz_LineChart_Embedding_Huge_Font.png differ diff --git a/e2e/support/helpers/e2e-ui-elements-helpers.js b/e2e/support/helpers/e2e-ui-elements-helpers.js index a6e1e6e04f47acc80e998133cb42f0785005a92e..9e29f720b7adc2c9a735a568635e636ea2ed12a3 100644 --- a/e2e/support/helpers/e2e-ui-elements-helpers.js +++ b/e2e/support/helpers/e2e-ui-elements-helpers.js @@ -133,7 +133,7 @@ export function clearFilterWidget(index = 0) { } export function resetFilterWidgetToDefault(index = 0) { - return filterWidget().eq(index).icon("time_history").click(); + return filterWidget().eq(index).icon("revert").click(); } export function setFilterWidgetValue( diff --git a/e2e/test/scenarios/dashboard-filters/dashboard-filters-reset-clear.cy.spec.ts b/e2e/test/scenarios/dashboard-filters/dashboard-filters-reset-clear.cy.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..628834a5b1f293ba0227588cbf5f3b829aee6296 --- /dev/null +++ b/e2e/test/scenarios/dashboard-filters/dashboard-filters-reset-clear.cy.spec.ts @@ -0,0 +1,814 @@ +import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database"; +import { + createQuestionAndDashboard, + dashboardParameterSidebar, + editDashboard, + popover, + restore, + updateDashboardCards, + visitDashboard, + type DashboardDetails, + type StructuredQuestionDetails, +} from "e2e/support/helpers"; +import { checkNotNull } from "metabase/lib/types"; +import type { LocalFieldReference } from "metabase-types/api"; + +const { ORDERS, ORDERS_ID, PEOPLE, PEOPLE_ID, PRODUCTS } = SAMPLE_DATABASE; + +const ORDERS_CREATED_AT_FIELD: LocalFieldReference = [ + "field", + ORDERS.CREATED_AT, + { + "base-type": "type/DateTime", + "temporal-unit": "month", + }, +]; + +const PEOPLE_ID_FIELD: LocalFieldReference = [ + "field", + PEOPLE.ID, + { + "base-type": "type/BigInteger", + }, +]; + +const PRODUCTS_CATEGORY_FIELD: LocalFieldReference = [ + "field", + PRODUCTS.CATEGORY, + { + "base-type": "type/Text", + }, +]; + +const PEOPLE_CITY_FIELD: LocalFieldReference = [ + "field", + PEOPLE.CITY, + { + "base-type": "type/Text", + }, +]; + +const ORDERS_COUNT_OVER_TIME: StructuredQuestionDetails = { + display: "line", + query: { + "source-table": ORDERS_ID, + aggregation: [["count"]], + breakout: [ORDERS_CREATED_AT_FIELD], + }, +}; + +const PEOPLE_QUESTION: StructuredQuestionDetails = { + query: { + "source-table": PEOPLE_ID, + limit: 1, + }, +}; + +const ORDERS_QUESTION: StructuredQuestionDetails = { + query: { + "source-table": ORDERS_ID, + limit: 1, + }, +}; + +const NO_DEFAULT_NON_REQUIRED = "no default value, non-required"; + +const DEFAULT_NON_REQUIRED = "default value, non-required"; + +const DEFAULT_REQUIRED = "default value, required"; + +describe("scenarios > dashboard > filters > reset & clear", () => { + beforeEach(() => { + restore(); + cy.signInAsAdmin(); + }); + + it("temporal unit parameters", () => { + createDashboardWithParameters( + ORDERS_COUNT_OVER_TIME, + ORDERS_CREATED_AT_FIELD, + [ + { + name: NO_DEFAULT_NON_REQUIRED, + slug: "no-default-value/non-required", + id: "fed1b910", + type: "temporal-unit", + sectionId: "temporal-unit", + }, + { + name: DEFAULT_NON_REQUIRED, + slug: "default-value/non-required", + id: "75d67d30", + type: "temporal-unit", + sectionId: "temporal-unit", + default: "year", + }, + { + name: DEFAULT_REQUIRED, + slug: "default-value/required", + id: "60f12ac0", + type: "temporal-unit", + sectionId: "temporal-unit", + default: "year", + required: true, + }, + ], + ); + + checkDashboardParameters({ + defaultValueFormatted: "Year", + otherValue: "Month", + otherValueFormatted: "Month", + setValue: (label, value) => { + filter(label).click(); + popover().findByText(value).click(); + }, + }); + }); + + it("time parameters", () => { + createDashboardWithParameters( + ORDERS_COUNT_OVER_TIME, + ORDERS_CREATED_AT_FIELD, + [ + { + name: NO_DEFAULT_NON_REQUIRED, + slug: "no-default-value/non-required", + id: "fed1b911", + type: "date/single", + sectionId: "date", + }, + { + name: DEFAULT_NON_REQUIRED, + slug: "default-value/non-required", + id: "75d67d31", + type: "date/single", + sectionId: "date", + default: "2024-01-01", + }, + { + name: DEFAULT_REQUIRED, + slug: "default-value/required", + id: "60f12ac1", + type: "date/single", + sectionId: "date", + default: "2024-01-01", + required: true, + }, + ], + ); + + checkDashboardParameters({ + defaultValueFormatted: "January 1, 2024", + otherValue: "01/01/2020", + otherValueFormatted: "January 1, 2020", + setValue: (label, value) => { + addDateFilter(label, value); + }, + updateValue: (label, value) => { + updateDateFilter(label, value); + }, + }); + }); + + it("location parameters - single value", () => { + createDashboardWithParameters(PEOPLE_QUESTION, PEOPLE_CITY_FIELD, [ + { + name: NO_DEFAULT_NON_REQUIRED, + slug: "no-default-value/non-required", + id: "fed1b912", + isMultiSelect: false, + type: "string/=", + sectionId: "location", + }, + { + name: DEFAULT_NON_REQUIRED, + slug: "default-value/non-required", + id: "75d67d32", + isMultiSelect: false, + type: "string/=", + sectionId: "location", + default: ["Bassett"], + }, + { + name: DEFAULT_REQUIRED, + slug: "default-value/required", + id: "60f12ac2", + isMultiSelect: false, + type: "string/=", + sectionId: "location", + required: true, + default: ["Bassett"], + }, + ]); + + checkDashboardParameters({ + defaultValueFormatted: "Bassett", + otherValue: "{backspace}Thomson", + otherValueFormatted: "Thomson", + setValue: (label, value) => { + filter(label).click(); + popover().findByRole("searchbox").clear().type(value).blur(); + popover().button("Add filter").click(); + }, + updateValue: (label, value) => { + filter(label).click(); + popover().findByRole("searchbox").clear().type(value).blur(); + popover().button("Update filter").click(); + }, + }); + }); + + it("location parameters - multiple values", () => { + createDashboardWithParameters(PEOPLE_QUESTION, PEOPLE_CITY_FIELD, [ + { + name: NO_DEFAULT_NON_REQUIRED, + slug: "no-default-value/non-required", + id: "fed1b913", + type: "string/=", + sectionId: "location", + }, + { + name: DEFAULT_NON_REQUIRED, + slug: "default-value/non-required", + id: "75d67d33", + type: "string/=", + sectionId: "location", + default: ["Bassett", "Thomson"], + }, + { + name: DEFAULT_REQUIRED, + slug: "default-value/required", + id: "60f12ac3", + type: "string/=", + sectionId: "location", + required: true, + default: ["Bassett", "Thomson"], + }, + ]); + + checkDashboardParameters({ + defaultValueFormatted: "2 selections", + otherValue: "{backspace}{backspace}Washington,", + otherValueFormatted: "Washington", + setValue: (label, value) => { + filter(label).click(); + popover().findByRole("searchbox").focus().type(value).blur(); + popover().button("Add filter").click(); + }, + updateValue: (label, value) => { + filter(label).click(); + popover().findByRole("searchbox").clear().type(value).blur(); + popover().button("Update filter").click(); + }, + }); + }); + + it("id parameters - single value", () => { + createDashboardWithParameters(PEOPLE_QUESTION, PEOPLE_ID_FIELD, [ + { + name: NO_DEFAULT_NON_REQUIRED, + slug: "no-default-value/non-required", + id: "fed1b914", + isMultiSelect: false, + type: "id", + sectionId: "id", + }, + { + name: DEFAULT_NON_REQUIRED, + slug: "default-value/non-required", + id: "75d67d34", + isMultiSelect: false, + type: "id", + sectionId: "id", + default: ["1"], + }, + { + name: DEFAULT_REQUIRED, + slug: "default-value/required", + id: "60f12ac4", + isMultiSelect: false, + type: "id", + sectionId: "id", + required: true, + default: ["1"], + }, + ]); + + checkDashboardParameters({ + defaultValueFormatted: "1", + otherValue: "{backspace}2", + otherValueFormatted: "2", + setValue: (label, value) => { + filter(label).click(); + popover().findByRole("searchbox").focus().type(value).blur(); + popover().button("Add filter").click(); + }, + updateValue: (label, value) => { + filter(label).click(); + popover().findByRole("searchbox").clear().type(value).blur(); + popover().button("Update filter").click(); + }, + }); + }); + + it("id parameters - multiple values", () => { + createDashboardWithParameters(PEOPLE_QUESTION, PEOPLE_ID_FIELD, [ + { + name: NO_DEFAULT_NON_REQUIRED, + slug: "no-default-value/non-required", + id: "fed1b915", + type: "id", + sectionId: "id", + }, + { + name: DEFAULT_NON_REQUIRED, + slug: "default-value/non-required", + id: "75d67d35", + type: "id", + sectionId: "id", + default: ["1", "2"], + }, + { + name: DEFAULT_REQUIRED, + slug: "default-value/required", + id: "60f12ac5", + type: "id", + sectionId: "id", + required: true, + default: ["1", "2"], + }, + ]); + + checkDashboardParameters({ + defaultValueFormatted: "2 selections", + otherValue: "{backspace}{backspace}3", + otherValueFormatted: "3", + setValue: (label, value) => { + filter(label).click(); + popover().findByRole("searchbox").focus().type(value).blur(); + popover().button("Add filter").click(); + }, + updateValue: (label, value) => { + filter(label).click(); + popover().findByRole("searchbox").clear().type(value).blur(); + popover().button("Update filter").click(); + }, + }); + }); + + it("number parameters - single value", () => { + createDashboardWithParameters(PEOPLE_QUESTION, PEOPLE_ID_FIELD, [ + { + name: NO_DEFAULT_NON_REQUIRED, + slug: "no-default-value/non-required", + id: "fed1b916", + type: "number/>=", + sectionId: "number", + }, + { + name: DEFAULT_NON_REQUIRED, + slug: "default-value/non-required", + id: "75d67d36", + type: "number/>=", + sectionId: "number", + default: [1], + }, + { + name: DEFAULT_REQUIRED, + slug: "default-value/required", + id: "60f12ac6", + type: "number/>=", + sectionId: "number", + required: true, + default: [1], + }, + ]); + + checkDashboardParameters({ + defaultValueFormatted: "1", + otherValue: "{backspace}2", + otherValueFormatted: "2", + setValue: (label, value) => { + filter(label).click(); + popover().findByRole("textbox").focus().type(value).blur(); + popover().button("Add filter").click(); + }, + updateValue: (label, value) => { + filter(label).click(); + popover().findByRole("textbox").focus().type(value).blur(); + popover().button("Update filter").click(); + }, + }); + }); + + it("number parameters - multiple values", () => { + createDashboardWithParameters(PEOPLE_QUESTION, PEOPLE_ID_FIELD, [ + { + name: NO_DEFAULT_NON_REQUIRED, + slug: "no-default-value/non-required", + id: "fed1b917", + type: "number/between", + sectionId: "number", + }, + { + name: DEFAULT_NON_REQUIRED, + slug: "default-value/non-required", + id: "75d67d37", + type: "number/between", + sectionId: "number", + default: [1, 2], + }, + { + name: DEFAULT_REQUIRED, + slug: "default-value/required", + id: "60f12ac7", + type: "number/between", + sectionId: "number", + required: true, + default: [1, 2], + }, + ]); + + checkDashboardParameters({ + defaultValueFormatted: "2 selections", + otherValue: ["3", "4"], + otherValueFormatted: "2 selections", + setValue: (label, [firstValue, secondValue]) => { + addRangeFilter(label, firstValue, secondValue); + }, + updateValue: (label, [firstValue, secondValue]) => { + updateRangeFilter(label, firstValue, secondValue); + }, + }); + }); + + it("text parameters - single value", () => { + createDashboardWithParameters(ORDERS_QUESTION, PRODUCTS_CATEGORY_FIELD, [ + { + name: NO_DEFAULT_NON_REQUIRED, + slug: "no-default-value/non-required", + id: "fed1b918", + isMultiSelect: false, + type: "string/=", + sectionId: "string", + }, + { + name: DEFAULT_NON_REQUIRED, + slug: "default-value/non-required", + id: "75d67d38", + isMultiSelect: false, + type: "string/=", + sectionId: "string", + default: ["Gizmo"], + }, + { + name: DEFAULT_REQUIRED, + slug: "default-value/required", + id: "60f12ac8", + isMultiSelect: false, + type: "string/=", + sectionId: "string", + required: true, + default: ["Gizmo"], + }, + ]); + + checkDashboardParameters({ + defaultValueFormatted: "Gizmo", + otherValue: "{backspace}Gadget,", + otherValueFormatted: "Gadget", + setValue: (label, value) => { + filter(label).click(); + popover().findByRole("combobox").type(value); + popover().button("Add filter").click(); + }, + updateValue: (label, value) => { + filter(label).click(); + popover().findByRole("combobox").type(value); + popover().button("Update filter").click(); + }, + }); + }); + + it("text parameters - multiple values", () => { + createDashboardWithParameters(ORDERS_QUESTION, PRODUCTS_CATEGORY_FIELD, [ + { + name: NO_DEFAULT_NON_REQUIRED, + slug: "no-default-value/non-required", + id: "fed1b919", + type: "string/=", + sectionId: "string", + }, + { + name: DEFAULT_NON_REQUIRED, + slug: "default-value/non-required", + id: "75d67d39", + type: "string/=", + sectionId: "string", + default: ["Gizmo", "Gadget"], + }, + { + name: DEFAULT_REQUIRED, + slug: "default-value/required", + id: "60f12ac9", + type: "string/=", + sectionId: "string", + required: true, + default: ["Gizmo", "Gadget"], + }, + ]); + + checkDashboardParameters({ + defaultValueFormatted: "2 selections", + otherValue: "{backspace}{backspace}Doohickey,Widget,", + otherValueFormatted: "2 selections", + setValue: (label, value) => { + filter(label).click(); + popover().findByRole("combobox").type(value); + popover().button("Add filter").click(); + }, + updateValue: (label, value) => { + filter(label).click(); + popover().findByRole("combobox").type(value); + popover().button("Update filter").click(); + }, + }); + }); + + it("chevron icons are aligned in temporal unit parameter sidebar", () => { + createDashboardWithParameters( + ORDERS_COUNT_OVER_TIME, + ORDERS_CREATED_AT_FIELD, + [ + { + name: "Unit of Time", + slug: "unit-of-time", + id: "fed1b910", + type: "temporal-unit", + sectionId: "temporal-unit", + }, + ], + ); + editDashboard(); + editFilter("Unit of Time"); + + dashboardParameterSidebar() + .findAllByLabelText("chevrondown icon") + .then(([$firstChevron, ...$otherChevrons]) => { + const firstRect = $firstChevron.getBoundingClientRect(); + + for (const $chevron of $otherChevrons) { + const rect = $chevron.getBoundingClientRect(); + + expect(firstRect.left, "left").to.eq(rect.left); + expect(firstRect.right, "right").to.eq(rect.right); + } + }); + }); + + function createDashboardWithParameters( + questionDetails: StructuredQuestionDetails, + targetField: LocalFieldReference, + parameters: DashboardDetails["parameters"], + ) { + createQuestionAndDashboard({ + questionDetails, + dashboardDetails: { + parameters, + }, + }).then(({ body: { dashboard_id, card_id } }) => { + updateDashboardCards({ + dashboard_id, + cards: [ + { + parameter_mappings: parameters?.map(parameter => ({ + parameter_id: parameter.id, + card_id: checkNotNull(card_id), + target: ["dimension", targetField], + })), + }, + ], + }); + + visitDashboard(dashboard_id); + }); + } + + function checkStatusIcon( + label: string, + /** + * Use 'none' when no icon should be visible. + */ + icon: "chevron" | "reset" | "clear" | "none", + ) { + clearIcon(label).should(icon === "clear" ? "be.visible" : "not.exist"); + resetIcon(label).should(icon === "reset" ? "be.visible" : "not.exist"); + chevronIcon(label).should(icon === "chevron" ? "be.visible" : "not.exist"); + } + + function checkDashboardParameters<T = string>({ + defaultValueFormatted, + otherValue, + otherValueFormatted, + setValue, + updateValue = setValue, + }: { + defaultValueFormatted: string; + otherValue: T; + otherValueFormatted: string; + setValue: (label: string, value: T) => void; + updateValue?: (label: string, value: T) => void; + }) { + cy.log("no default value, non-required, no current value"); + checkStatusIcon(NO_DEFAULT_NON_REQUIRED, "chevron"); + + cy.log("no default value, non-required, has current value"); + setValue(NO_DEFAULT_NON_REQUIRED, otherValue); + filter(NO_DEFAULT_NON_REQUIRED).should("have.text", otherValueFormatted); + checkStatusIcon(NO_DEFAULT_NON_REQUIRED, "clear"); + clearButton(NO_DEFAULT_NON_REQUIRED).click(); + filter(NO_DEFAULT_NON_REQUIRED).should( + "have.text", + NO_DEFAULT_NON_REQUIRED, + ); + checkStatusIcon(NO_DEFAULT_NON_REQUIRED, "chevron"); + + cy.log("has default value, non-required, current value same as default"); + checkStatusIcon(DEFAULT_NON_REQUIRED, "clear"); + filter(DEFAULT_NON_REQUIRED).should("have.text", defaultValueFormatted); + clearButton(DEFAULT_NON_REQUIRED).click(); + filter(DEFAULT_NON_REQUIRED).should("have.text", DEFAULT_NON_REQUIRED); + + cy.log("has default value, non-required, no current value"); + checkStatusIcon(DEFAULT_NON_REQUIRED, "reset"); + resetButton(DEFAULT_NON_REQUIRED).click(); + filter(DEFAULT_NON_REQUIRED).should("have.text", defaultValueFormatted); + checkStatusIcon(DEFAULT_NON_REQUIRED, "clear"); + + cy.log( + "has default value, non-required, current value different than default", + ); + updateValue(DEFAULT_NON_REQUIRED, otherValue); + filter(DEFAULT_NON_REQUIRED).should("have.text", otherValueFormatted); + checkStatusIcon(DEFAULT_NON_REQUIRED, "reset"); + resetButton(DEFAULT_NON_REQUIRED).click(); + filter(DEFAULT_NON_REQUIRED).should("have.text", defaultValueFormatted); + checkStatusIcon(DEFAULT_NON_REQUIRED, "clear"); + + cy.log("has default value, required, value same as default"); + checkStatusIcon(DEFAULT_REQUIRED, "none"); + + cy.log("has default value, required, current value different than default"); + updateValue(DEFAULT_REQUIRED, otherValue); + filter(DEFAULT_REQUIRED).should("have.text", otherValueFormatted); + checkStatusIcon(DEFAULT_REQUIRED, "reset"); + resetButton(DEFAULT_REQUIRED).click(); + filter(DEFAULT_REQUIRED).should("have.text", defaultValueFormatted); + checkStatusIcon(DEFAULT_REQUIRED, "none"); + + checkParameterSidebarDefaultValue({ + defaultValueFormatted, + otherValue, + otherValueFormatted, + setValue, + updateValue, + }); + } + + function checkParameterSidebarDefaultValue<T = string>({ + defaultValueFormatted, + otherValue, + otherValueFormatted, + setValue, + updateValue, + }: { + defaultValueFormatted: string; + otherValue: T; + otherValueFormatted: string; + setValue: (label: string, value: T) => void; + updateValue: (label: string, value: T) => void; + }) { + cy.log("parameter sidebar"); + editDashboard(); + + cy.log(NO_DEFAULT_NON_REQUIRED); + editFilter(NO_DEFAULT_NON_REQUIRED); + dashboardParameterSidebar().within(() => { + filter("Default value").scrollIntoView(); + filter("Default value").should("have.text", "No default"); + checkStatusIcon("Default value", "chevron"); + }); + + setValue("Default value", otherValue); + + dashboardParameterSidebar().within(() => { + filter("Default value").should("have.text", otherValueFormatted); + checkStatusIcon("Default value", "clear"); + + clearButton("Default value").click(); + filter("Default value").should("have.text", "No default"); + checkStatusIcon("Default value", "chevron"); + }); + + cy.log(DEFAULT_NON_REQUIRED); + editFilter(DEFAULT_NON_REQUIRED); + dashboardParameterSidebar().within(() => { + filter("Default value").should("have.text", defaultValueFormatted); + checkStatusIcon("Default value", "clear"); + + clearButton("Default value").click(); + filter("Default value").should("have.text", "No default"); + checkStatusIcon("Default value", "chevron"); + }); + + setValue("Default value", otherValue); + + dashboardParameterSidebar().within(() => { + filter("Default value").should("have.text", otherValueFormatted); + checkStatusIcon("Default value", "clear"); + }); + + cy.log(DEFAULT_REQUIRED); + editFilter(DEFAULT_REQUIRED); + dashboardParameterSidebar().within(() => { + filter("Default value").should("have.text", defaultValueFormatted); + checkStatusIcon("Default value", "clear"); + + clearButton("Default value").click(); + filter("Default value (required)").should("have.text", "No default"); + checkStatusIcon("Default value (required)", "chevron"); + }); + + updateValue("Default value (required)", otherValue); + + dashboardParameterSidebar().within(() => { + filter("Default value").should("have.text", otherValueFormatted); + checkStatusIcon("Default value", "clear"); + }); + } + + function filter(label: string) { + return cy.findByLabelText(label); + } + + function editFilter(label: string) { + cy.findByTestId("edit-dashboard-parameters-widget-container") + .findByText(label) + .click(); + } + + function clearIcon(label: string) { + return filter(label).icon("close"); + } + + function resetIcon(label: string) { + return filter(label).icon("revert"); + } + + function clearButton(label: string) { + return filter(label).findByLabelText("Clear"); + } + + function resetButton(label: string) { + return filter(label).findByLabelText("Reset filter to default state"); + } + + function chevronIcon(label: string) { + return filter(label).icon("chevrondown"); + } + + function addDateFilter(label: string, value: string) { + filter(label).click(); + popover().findByRole("textbox").clear().type(value).blur(); + popover().button("Add filter").click(); + } + + function updateDateFilter(label: string, value: string) { + filter(label).click(); + popover().findByRole("textbox").clear().type(value).blur(); + popover().button("Update filter").click(); + } + + function addRangeFilter( + label: string, + firstValue: string, + secondValue: string, + ) { + filter(label).click(); + popover().findAllByRole("textbox").first().clear().type(firstValue).blur(); + popover().findAllByRole("textbox").last().clear().type(secondValue).blur(); + popover().button("Add filter").click(); + } + + function updateRangeFilter( + label: string, + firstValue: string, + secondValue: string, + ) { + filter(label).click(); + popover().findAllByRole("textbox").first().clear().type(firstValue).blur(); + popover().findAllByRole("textbox").last().clear().type(secondValue).blur(); + popover().button("Update filter").click(); + } +}); diff --git a/e2e/test/scenarios/filters-reproductions/dashboard-filters-reproductions.cy.spec.js b/e2e/test/scenarios/filters-reproductions/dashboard-filters-reproductions.cy.spec.js index 8f3d9ee3b5b9f3de22153e9429a38ec393ecdf3a..1c80a3588caee404fb6e1ab5568ff9c9fbd4d026 100644 --- a/e2e/test/scenarios/filters-reproductions/dashboard-filters-reproductions.cy.spec.js +++ b/e2e/test/scenarios/filters-reproductions/dashboard-filters-reproductions.cy.spec.js @@ -1779,8 +1779,7 @@ describe("issue 25374", () => { it("should pass comma-separated values down to the connected question (metabase#25374-1)", () => { // Drill-through and go to the question - // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage - cy.findByText(questionDetails.name).click(); + getDashboardCard(0).findByText(questionDetails.name).click(); cy.wait("@cardQuery"); cy.get("[data-testid=cell-data]") @@ -1804,6 +1803,31 @@ describe("issue 25374", () => { // Make sure URL search params are correct cy.location("search").should("eq", "?equal_to=1%2C2%2C3"); }); + + it("should retain comma-separated values when reverting to default (metabase#25374-3)", () => { + editDashboard(); + cy.findByTestId("edit-dashboard-parameters-widget-container") + .findByText("Equal to") + .click(); + dashboardParameterSidebar().findByLabelText("Default value").type("1,2,3"); + saveDashboard(); + + cy.button("Clear").click(); + cy.location("search").should("eq", "?equal_to="); + + cy.button("Reset filter to default state").click(); + cy.location("search").should("eq", "?equal_to=1%2C2%2C3"); + + // Drill-through and go to the question + getDashboardCard(0).findByText(questionDetails.name).click(); + cy.wait("@cardQuery"); + + cy.get("[data-testid=cell-data]") + .should("contain", "COUNT(*)") + .and("contain", "3"); + + cy.location("search").should("eq", "?num=1%2C2%2C3"); + }); }); describe("issue 25908", () => { const questionDetails = { diff --git a/e2e/test/scenarios/native-filters/sql-field-filter.cy.spec.js b/e2e/test/scenarios/native-filters/sql-field-filter.cy.spec.js index 71722179b89748364770999fd93d576170172b08..e8aaf9923db39ae6e009c663375cddc59319dc87 100644 --- a/e2e/test/scenarios/native-filters/sql-field-filter.cy.spec.js +++ b/e2e/test/scenarios/native-filters/sql-field-filter.cy.spec.js @@ -87,7 +87,7 @@ describe("scenarios > filters > sql filters > field filter", () => { multiAutocompleteInput().type("10,"); cy.findByText("Update filter").click(); }); - filterWidget().icon("time_history").click(); + filterWidget().icon("revert").click(); filterWidget().findByTestId("field-set-content").should("have.text", "8"); }); }); diff --git a/e2e/test/scenarios/native-filters/sql-filters-reset-clear.cy.spec.ts b/e2e/test/scenarios/native-filters/sql-filters-reset-clear.cy.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..16c694b8198f5786d5bb1487ae0c700e7f1d538a --- /dev/null +++ b/e2e/test/scenarios/native-filters/sql-filters-reset-clear.cy.spec.ts @@ -0,0 +1,810 @@ +import { SAMPLE_DATABASE } from "e2e/support/cypress_sample_database"; +import { createNativeQuestion, popover, restore } from "e2e/support/helpers"; +import type { TemplateTag } from "metabase-types/api"; + +type SectionId = + | "no_default_non_required" + | "no_default_required" + | "default_non_required" + | "default_required"; + +const { PRODUCTS } = SAMPLE_DATABASE; + +const NO_DEFAULT_NON_REQUIRED = "no default value, non-required"; + +// unlike required dashboard filter, required native filter doesn't need to have a default value +const NO_DEFAULT_REQUIRED = "no default value, required"; + +const DEFAULT_NON_REQUIRED = "default value, non-required"; + +const DEFAULT_REQUIRED = "default value, required"; + +describe("scenarios > filters > sql filters > reset & clear", () => { + beforeEach(() => { + restore(); + cy.signInAsAdmin(); + }); + + it("text parameters", () => { + createNativeQuestionWithParameters({ + no_default_non_required: { + name: "no_default_non_required", + "display-name": NO_DEFAULT_NON_REQUIRED, + id: "fed1b918", + type: "text", + }, + no_default_required: { + name: "no_default_required", + "display-name": NO_DEFAULT_REQUIRED, + id: "fed1b919", + type: "text", + required: true, + }, + default_non_required: { + name: "default_non_required", + "display-name": DEFAULT_NON_REQUIRED, + id: "75d67d38", + type: "text", + default: "a", + }, + default_required: { + name: "default_required", + "display-name": DEFAULT_REQUIRED, + id: "60f12ac8", + type: "text", + required: true, + default: "a", + }, + }); + + checkNativeParametersInput({ + defaultValueFormatted: "a", + otherValue: "{backspace}b", + otherValueFormatted: "b", + setValue: (label, value) => { + filterInput(label).focus().clear().type(value).blur(); + }, + }); + }); + + it("number parameters", () => { + createNativeQuestionWithParameters({ + no_default_non_required: { + name: "no_default_non_required", + "display-name": NO_DEFAULT_NON_REQUIRED, + id: "fed1b918", + type: "number", + }, + no_default_required: { + name: "no_default_required", + "display-name": NO_DEFAULT_REQUIRED, + id: "fed1b919", + type: "number", + required: true, + }, + default_non_required: { + name: "default_non_required", + "display-name": DEFAULT_NON_REQUIRED, + id: "75d67d38", + type: "number", + default: "1", + }, + default_required: { + name: "default_required", + "display-name": DEFAULT_REQUIRED, + id: "60f12ac8", + type: "number", + required: true, + default: "1", + }, + }); + + checkNativeParametersInput({ + defaultValueFormatted: "1", + otherValue: "{backspace}2", + otherValueFormatted: "2", + setValue: (label, value) => { + filterInput(label).focus().clear().type(value).blur(); + }, + }); + }); + + it("date parameters", () => { + createNativeQuestionWithParameters({ + no_default_non_required: { + name: "no_default_non_required", + "display-name": NO_DEFAULT_NON_REQUIRED, + id: "fed1b918", + type: "date", + }, + no_default_required: { + name: "no_default_required", + "display-name": NO_DEFAULT_REQUIRED, + id: "fed1b919", + type: "date", + required: true, + }, + default_non_required: { + name: "default_non_required", + "display-name": DEFAULT_NON_REQUIRED, + id: "75d67d38", + type: "date", + default: "2024-01-01", + }, + default_required: { + name: "default_required", + "display-name": DEFAULT_REQUIRED, + id: "60f12ac8", + type: "date", + required: true, + default: "2024-01-01", + }, + }); + + checkNativeParametersDropdown({ + defaultValueFormatted: "January 1, 2024", + otherValue: "01/01/2020", + otherValueFormatted: "January 1, 2020", + setValue: value => { + addDateFilter(value); + }, + updateValue: value => { + updateDateFilter(value); + }, + }); + + checkParameterSidebarDefaultValueDate({ + defaultValueFormatted: "January 1, 2024", + otherValue: "01/01/2020", + otherValueFormatted: "January 1, 2020", + }); + }); + + it("field parameters", () => { + createNativeQuestionWithParameters({ + no_default_non_required: { + name: "no_default_non_required", + "display-name": NO_DEFAULT_NON_REQUIRED, + id: "fed1b918", + type: "dimension", + dimension: ["field", PRODUCTS.CATEGORY, null], + "widget-type": "string/contains", + options: { "case-sensitive": false }, + }, + no_default_required: { + name: "no_default_required", + "display-name": NO_DEFAULT_REQUIRED, + id: "fed1b919", + type: "dimension", + dimension: ["field", PRODUCTS.CATEGORY, null], + "widget-type": "string/contains", + options: { "case-sensitive": false }, + required: true, + }, + default_non_required: { + name: "default_non_required", + "display-name": DEFAULT_NON_REQUIRED, + id: "75d67d38", + type: "dimension", + dimension: ["field", PRODUCTS.CATEGORY, null], + "widget-type": "string/contains", + options: { "case-sensitive": false }, + // @ts-expect-error - TODO: https://github.com/metabase/metabase/issues/46263 + default: ["Gizmo"], + }, + default_required: { + name: "default_required", + "display-name": DEFAULT_REQUIRED, + id: "60f12ac8", + type: "dimension", + dimension: ["field", PRODUCTS.CATEGORY, null], + "widget-type": "string/contains", + options: { "case-sensitive": false }, + required: true, + // @ts-expect-error - TODO: https://github.com/metabase/metabase/issues/46263 + default: ["Gizmo"], + }, + }); + + checkNativeParametersDropdown({ + defaultValueFormatted: "Gizmo", + otherValue: "{backspace}Gadget", + otherValueFormatted: "Gadget", + setValue: value => { + popover().findByRole("searchbox").clear().type(value).blur(); + popover().button("Add filter").click(); + }, + updateValue: value => { + popover().findByRole("searchbox").clear().type(value).blur(); + popover().button("Update filter").click(); + }, + }); + + checkParameterSidebarDefaultValueDropdown({ + defaultValueFormatted: "Gizmo", + otherValue: "{backspace}Gadget", + otherValueFormatted: "Gadget", + setValue: value => { + popover().findByRole("searchbox").clear().type(value).blur(); + popover().button("Add filter").click(); + }, + }); + }); + + function createNativeQuestionWithParameters( + templateTags: Record<SectionId, TemplateTag>, + ) { + createNativeQuestion( + { + native: { + query: + "select {{no_default_non_required}}, {{no_default_required}}, {{default_non_required}}, {{default_required}}", + "template-tags": templateTags, + }, + }, + { visitQuestion: true }, + ); + } + + function checkStatusIcon( + label: string, + /** + * Use 'none' when no icon should be visible. + */ + icon: "chevron" | "reset" | "clear" | "none", + ) { + clearIcon(label).should(icon === "clear" ? "be.visible" : "not.exist"); + resetIcon(label).should(icon === "reset" ? "be.visible" : "not.exist"); + chevronIcon(label).should(icon === "chevron" ? "be.visible" : "not.exist"); + } + + function checkNativeParametersInput({ + defaultValueFormatted, + otherValue, + otherValueFormatted, + setValue, + updateValue = setValue, + }: { + defaultValueFormatted: string; + otherValue: string; + otherValueFormatted: string; + setValue: (label: string, value: string) => void; + updateValue?: (label: string, value: string) => void; + }) { + cy.log("no default value, non-required, no current value"); + checkStatusIcon(NO_DEFAULT_NON_REQUIRED, "none"); + + cy.log("no default value, non-required, has current value"); + setValue(NO_DEFAULT_NON_REQUIRED, otherValue); + filterInput(NO_DEFAULT_NON_REQUIRED).should( + "have.value", + otherValueFormatted, + ); + checkStatusIcon(NO_DEFAULT_NON_REQUIRED, "clear"); + clearButton(NO_DEFAULT_NON_REQUIRED).click(); + filterInput(NO_DEFAULT_NON_REQUIRED).should("have.value", ""); + checkStatusIcon(NO_DEFAULT_NON_REQUIRED, "none"); + + cy.log("no default value, required, no current value"); + checkStatusIcon(NO_DEFAULT_REQUIRED, "none"); + + cy.log("no default value, required, has current value"); + updateValue(NO_DEFAULT_REQUIRED, otherValue); + filterInput(NO_DEFAULT_REQUIRED).should("have.value", otherValueFormatted); + checkStatusIcon(NO_DEFAULT_REQUIRED, "clear"); + + cy.log("has default value, non-required, current value same as default"); + checkStatusIcon(DEFAULT_NON_REQUIRED, "clear"); + filterInput(DEFAULT_NON_REQUIRED).should( + "have.value", + defaultValueFormatted, + ); + clearButton(DEFAULT_NON_REQUIRED).click(); + filterInput(DEFAULT_NON_REQUIRED).should("have.value", ""); + + cy.log("has default value, non-required, no current value"); + checkStatusIcon(DEFAULT_NON_REQUIRED, "reset"); + resetButton(DEFAULT_NON_REQUIRED).click(); + filterInput(DEFAULT_NON_REQUIRED).should( + "have.value", + defaultValueFormatted, + ); + checkStatusIcon(DEFAULT_NON_REQUIRED, "clear"); + + cy.log( + "has default value, non-required, current value different than default", + ); + updateValue(DEFAULT_NON_REQUIRED, otherValue); + filterInput(DEFAULT_NON_REQUIRED).should("have.value", otherValueFormatted); + checkStatusIcon(DEFAULT_NON_REQUIRED, "reset"); + resetButton(DEFAULT_NON_REQUIRED).click(); + filterInput(DEFAULT_NON_REQUIRED).should( + "have.value", + defaultValueFormatted, + ); + checkStatusIcon(DEFAULT_NON_REQUIRED, "clear"); + + cy.log("has default value, required, value same as default"); + checkStatusIcon(DEFAULT_REQUIRED, "none"); + + cy.log("has default value, required, current value different than default"); + updateValue(DEFAULT_REQUIRED, otherValue); + filterInput(DEFAULT_REQUIRED).should("have.value", otherValueFormatted); + checkStatusIcon(DEFAULT_REQUIRED, "reset"); + resetButton(DEFAULT_REQUIRED).click(); + filterInput(DEFAULT_REQUIRED).should("have.value", defaultValueFormatted); + checkStatusIcon(DEFAULT_REQUIRED, "none"); + + checkParameterSidebarDefaultValue({ + defaultValueFormatted, + otherValue, + otherValueFormatted, + setValue, + updateValue, + }); + } + + function checkNativeParametersDropdown({ + defaultValueFormatted, + otherValue, + otherValueFormatted, + setValue, + updateValue = setValue, + }: { + defaultValueFormatted: string; + otherValue: string; + otherValueFormatted: string; + setValue: (value: string) => void; + updateValue?: (value: string) => void; + }) { + cy.log("no default value, non-required, no current value"); + checkStatusIcon(NO_DEFAULT_NON_REQUIRED, "chevron"); + + cy.log("no default value, non-required, has current value"); + filter(NO_DEFAULT_NON_REQUIRED).click(); + setValue(otherValue); + filter(NO_DEFAULT_NON_REQUIRED).should("have.text", otherValueFormatted); + checkStatusIcon(NO_DEFAULT_NON_REQUIRED, "clear"); + clearButton(NO_DEFAULT_NON_REQUIRED).click(); + filter(NO_DEFAULT_NON_REQUIRED).should( + "have.text", + NO_DEFAULT_NON_REQUIRED, + ); + checkStatusIcon(NO_DEFAULT_NON_REQUIRED, "chevron"); + + cy.log("no default value, required, no current value"); + checkStatusIcon(NO_DEFAULT_REQUIRED, "none"); + + cy.log("no default value, required, has current value"); + filter(NO_DEFAULT_REQUIRED).click(); + updateValue(otherValue); + filter(NO_DEFAULT_REQUIRED).should("have.text", otherValueFormatted); + // checkStatusIcon(NO_DEFAULT_REQUIRED, "clear"); + + cy.log("has default value, non-required, current value same as default"); + checkStatusIcon(DEFAULT_NON_REQUIRED, "clear"); + filter(DEFAULT_NON_REQUIRED).should("have.text", defaultValueFormatted); + clearButton(DEFAULT_NON_REQUIRED).click(); + filter(DEFAULT_NON_REQUIRED).should("have.text", DEFAULT_NON_REQUIRED); + + cy.log("has default value, non-required, no current value"); + checkStatusIcon(DEFAULT_NON_REQUIRED, "reset"); + resetButton(DEFAULT_NON_REQUIRED).click(); + filter(DEFAULT_NON_REQUIRED).should("have.text", defaultValueFormatted); + checkStatusIcon(DEFAULT_NON_REQUIRED, "clear"); + + cy.log( + "has default value, non-required, current value different than default", + ); + filter(DEFAULT_NON_REQUIRED).click(); + updateValue(otherValue); + filter(DEFAULT_NON_REQUIRED).should("have.text", otherValueFormatted); + checkStatusIcon(DEFAULT_NON_REQUIRED, "reset"); + resetButton(DEFAULT_NON_REQUIRED).click(); + filter(DEFAULT_NON_REQUIRED).should("have.text", defaultValueFormatted); + checkStatusIcon(DEFAULT_NON_REQUIRED, "clear"); + + cy.log("has default value, required, value same as default"); + checkStatusIcon(DEFAULT_REQUIRED, "none"); + + cy.log("has default value, required, current value different than default"); + filter(DEFAULT_REQUIRED).click(); + updateValue(otherValue); + filter(DEFAULT_REQUIRED).should("have.text", otherValueFormatted); + checkStatusIcon(DEFAULT_REQUIRED, "reset"); + resetButton(DEFAULT_REQUIRED).click(); + filter(DEFAULT_REQUIRED).should("have.text", defaultValueFormatted); + checkStatusIcon(DEFAULT_REQUIRED, "none"); + } + + function checkParameterSidebarDefaultValue<T = string>({ + defaultValueFormatted, + otherValue, + otherValueFormatted, + setValue, + updateValue, + }: { + defaultValueFormatted: string; + otherValue: T; + otherValueFormatted: string; + setValue: (label: string, value: T) => void; + updateValue: (label: string, value: T) => void; + }) { + cy.log("parameter sidebar"); + + cy.findByTestId("visibility-toggler").click(); + cy.icon("variable").click(); + + cy.log(NO_DEFAULT_NON_REQUIRED); + filterSection("no_default_non_required").within(() => { + filter("Default filter widget value").scrollIntoView(); + filterInput("Default filter widget value").should("have.value", ""); + checkStatusIcon("Default filter widget value", "none"); + + setValue("Default filter widget value", otherValue); + filterInput("Default filter widget value").should( + "have.value", + otherValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + + clearIcon("Default filter widget value").click(); + filterInput("Default filter widget value").should("have.value", ""); + checkStatusIcon("Default filter widget value", "none"); + }); + + cy.log(DEFAULT_NON_REQUIRED); + filterSection("default_non_required").within(() => { + filter("Default filter widget value").scrollIntoView(); + filterInput("Default filter widget value").should( + "have.value", + defaultValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + + clearIcon("Default filter widget value").click(); + filterInput("Default filter widget value").should("have.value", ""); + checkStatusIcon("Default filter widget value", "none"); + + setValue("Default filter widget value", otherValue); + filterInput("Default filter widget value").should( + "have.value", + otherValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + }); + + cy.log(DEFAULT_REQUIRED); + filterSection("default_required").within(() => { + filter("Default filter widget value").scrollIntoView(); + filterInput("Default filter widget value").should( + "have.value", + defaultValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + + clearIcon("Default filter widget value").click(); + filterInput("Default filter widget value (required)").should( + "have.value", + "", + ); + checkStatusIcon("Default filter widget value (required)", "none"); + + updateValue("Default filter widget value (required)", otherValue); + filterInput("Default filter widget value").should( + "have.value", + otherValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + }); + + cy.log(NO_DEFAULT_REQUIRED); + filterSection("no_default_required").within(() => { + filter("Default filter widget value (required)").scrollIntoView(); + filterInput("Default filter widget value (required)").should( + "have.value", + "", + ); + checkStatusIcon("Default filter widget value (required)", "none"); + + updateValue("Default filter widget value (required)", otherValue); + filterInput("Default filter widget value").should( + "have.value", + otherValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + + clearButton("Default filter widget value").click(); + checkStatusIcon("Default filter widget value (required)", "none"); + }); + } + + function checkParameterSidebarDefaultValueDate({ + defaultValueFormatted, + otherValue, + otherValueFormatted, + }: { + defaultValueFormatted: string; + otherValue: string; + otherValueFormatted: string; + }) { + cy.log("parameter sidebar"); + + cy.findByTestId("visibility-toggler").click(); + cy.icon("variable").click(); + + cy.log(NO_DEFAULT_NON_REQUIRED); + filterSection("no_default_non_required").within(() => { + filter("Default filter widget value").scrollIntoView(); + filterInput("Default filter widget value").should("have.value", ""); + checkStatusIcon("Default filter widget value", "chevron"); + filter("Default filter widget value").click(); + }); + + popover().findByRole("textbox").clear().type(otherValue).blur(); + popover().button("Add filter").click(); + + filterSection("no_default_non_required").within(() => { + filterInput("Default filter widget value").should( + "have.value", + otherValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + + clearIcon("Default filter widget value").click(); + filterInput("Default filter widget value").should("have.value", ""); + checkStatusIcon("Default filter widget value", "chevron"); + }); + + cy.log(NO_DEFAULT_REQUIRED); + filterSection("no_default_required").within(() => { + filter("Default filter widget value (required)").scrollIntoView(); + filterInput("Default filter widget value (required)").should( + "have.value", + "", + ); + checkStatusIcon("Default filter widget value (required)", "chevron"); + filter("Default filter widget value (required)").click(); + }); + + popover().findByRole("textbox").clear().type(otherValue).blur(); + popover().button("Add filter").click(); + + filterSection("no_default_required").within(() => { + filterInput("Default filter widget value").should( + "have.value", + otherValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + + clearButton("Default filter widget value").click(); + checkStatusIcon("Default filter widget value (required)", "chevron"); + }); + + cy.log(DEFAULT_NON_REQUIRED); + filterSection("default_non_required").within(() => { + filter("Default filter widget value").scrollIntoView(); + filterInput("Default filter widget value").should( + "have.value", + defaultValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + + clearIcon("Default filter widget value").click(); + filterInput("Default filter widget value").should("have.value", ""); + checkStatusIcon("Default filter widget value", "chevron"); + filter("Default filter widget value").click(); + }); + + popover().findByRole("textbox").clear().type(otherValue).blur(); + popover().button("Add filter").click(); + + filterSection("default_non_required").within(() => { + filterInput("Default filter widget value").should( + "have.value", + otherValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + }); + + cy.log(DEFAULT_REQUIRED); + filterSection("default_required").within(() => { + filter("Default filter widget value").scrollIntoView(); + filterInput("Default filter widget value").should( + "have.value", + defaultValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + + clearIcon("Default filter widget value").click(); + filterInput("Default filter widget value (required)").should( + "have.value", + "", + ); + checkStatusIcon("Default filter widget value (required)", "chevron"); + filter("Default filter widget value (required)").click(); + }); + + popover().findByRole("textbox").clear().type(otherValue).blur(); + popover().button("Add filter").click(); + + filterSection("default_required").within(() => { + filterInput("Default filter widget value").should( + "have.value", + otherValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + }); + } + + function checkParameterSidebarDefaultValueDropdown({ + defaultValueFormatted, + otherValue, + otherValueFormatted, + setValue, + updateValue = setValue, + }: { + defaultValueFormatted: string; + otherValue: string; + otherValueFormatted: string; + setValue: (value: string) => void; + updateValue?: (value: string) => void; + }) { + cy.log("parameter sidebar"); + + cy.findByTestId("visibility-toggler").click(); + cy.icon("variable").click(); + + cy.log(NO_DEFAULT_NON_REQUIRED); + filterSection("no_default_non_required").within(() => { + filter("Default filter widget value").scrollIntoView(); + filter("Default filter widget value").should( + "have.text", + "Enter a default value…", + ); + checkStatusIcon("Default filter widget value", "chevron"); + filter("Default filter widget value").click(); + }); + + setValue(otherValue); + + filterSection("no_default_non_required").within(() => { + filter("Default filter widget value").should( + "have.text", + otherValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + + clearIcon("Default filter widget value").click(); + filter("Default filter widget value").should( + "have.text", + "Enter a default value…", + ); + checkStatusIcon("Default filter widget value", "chevron"); + }); + + cy.log(NO_DEFAULT_REQUIRED); + filterSection("no_default_required").within(() => { + filter("Default filter widget value (required)").scrollIntoView(); + filter("Default filter widget value (required)").should( + "have.text", + "Enter a default value…", + ); + checkStatusIcon("Default filter widget value (required)", "chevron"); + filter("Default filter widget value (required)").click(); + }); + + setValue(otherValue); + + filterSection("no_default_required").within(() => { + filter("Default filter widget value").should( + "have.text", + otherValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + + clearButton("Default filter widget value").click(); + checkStatusIcon("Default filter widget value (required)", "chevron"); + }); + + cy.log(DEFAULT_NON_REQUIRED); + filterSection("default_non_required").within(() => { + filter("Default filter widget value").scrollIntoView(); + filter("Default filter widget value").should( + "have.text", + defaultValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + + clearIcon("Default filter widget value").click(); + filter("Default filter widget value").should( + "have.text", + "Enter a default value…", + ); + checkStatusIcon("Default filter widget value", "chevron"); + filter("Default filter widget value").click(); + }); + + setValue(otherValue); + + filterSection("default_non_required").within(() => { + filter("Default filter widget value").should( + "have.text", + otherValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + }); + + cy.log(DEFAULT_REQUIRED); + filterSection("default_required").within(() => { + filter("Default filter widget value").scrollIntoView(); + filter("Default filter widget value").should( + "have.text", + defaultValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + + clearIcon("Default filter widget value").click(); + filter("Default filter widget value (required)").should( + "have.text", + "Enter a default value…", + ); + checkStatusIcon("Default filter widget value (required)", "chevron"); + filter("Default filter widget value (required)").click(); + }); + + updateValue(otherValue); + + filterSection("default_required").within(() => { + filter("Default filter widget value").should( + "have.text", + otherValueFormatted, + ); + checkStatusIcon("Default filter widget value", "clear"); + }); + } + + function filter(label: string) { + return cy.findByLabelText(label); + } + + function filterInput(label: string) { + return filter(label).findByRole("textbox"); + } + + function filterSection(id: SectionId) { + return cy.findByTestId(`tag-editor-variable-${id}`); + } + + function clearIcon(label: string) { + return filter(label).parent().icon("close"); + } + + function resetIcon(label: string) { + return filter(label).parent().icon("revert"); + } + + function clearButton(label: string) { + return filter(label).parent().findByLabelText("Clear"); + } + + function resetButton(label: string) { + return filter(label) + .parent() + .findByLabelText("Reset filter to default state"); + } + + function chevronIcon(label: string) { + return filter(label).parent().icon("chevrondown"); + } + + function addDateFilter(value: string) { + popover().findByRole("textbox").clear().type(value).blur(); + popover().button("Add filter").click(); + } + + function updateDateFilter(value: string) { + popover().findByRole("textbox").clear().type(value).blur(); + popover().button("Update filter").click(); + } +}); diff --git a/e2e/test/scenarios/native-filters/sql-filters.cy.spec.js b/e2e/test/scenarios/native-filters/sql-filters.cy.spec.js index 9edfeed481289d6a6ce2be021748ba5b4c74325f..612dd2e6d6e822cf991d52114dca8df9ce18ac87 100644 --- a/e2e/test/scenarios/native-filters/sql-filters.cy.spec.js +++ b/e2e/test/scenarios/native-filters/sql-filters.cy.spec.js @@ -78,7 +78,7 @@ describe("scenarios > filters > sql filters > basic filter types", () => { SQLFilter.toggleRequired(); filterWidget().within(() => { cy.get("input").type("abc").should("have.value", "defaultabc"); - cy.icon("time_history").click(); + cy.icon("revert").click(); cy.get("input").should("have.value", "default"); }); }); @@ -148,7 +148,7 @@ describe("scenarios > filters > sql filters > basic filter types", () => { SQLFilter.toggleRequired(); filterWidget().within(() => { cy.get("input").type(".11").should("have.value", "3.11"); - cy.icon("time_history").click(); + cy.icon("revert").click(); cy.get("input").should("have.value", "3"); }); }); @@ -234,7 +234,7 @@ describe("scenarios > filters > sql filters > basic filter types", () => { cy.findByText("15").click(); cy.findByText("Update filter").click(); }); - filterWidget().icon("time_history").click(); + filterWidget().icon("revert").click(); filterWidget() .findByTestId("field-set-content") .should("have.text", "November 1, 2023"); diff --git a/frontend/src/metabase-lib/v1/parameters/utils/parameter-values.js b/frontend/src/metabase-lib/v1/parameters/utils/parameter-values.js index a12f9d1059b337750a44fbd49ca128246ae0e193..40bed4d8ceb5c74dfe0cc7d91df4ecead9a800a6 100644 --- a/frontend/src/metabase-lib/v1/parameters/utils/parameter-values.js +++ b/frontend/src/metabase-lib/v1/parameters/utils/parameter-values.js @@ -123,7 +123,11 @@ export function isParameterValueEmpty(value) { // Should treat undefined and null equally. // TODO reconcile with isParameterValueEmpty export function parameterHasNoDisplayValue(value) { - return !value || value === "" || (Array.isArray(value) && value.length === 0); + return ( + (!value && value !== 0) || + value === "" || + (Array.isArray(value) && value.length === 0) + ); } export function normalizeParameterValue(type, value) { diff --git a/frontend/src/metabase/parameters/components/ParameterSettings/ParameterSettings.styled.tsx b/frontend/src/metabase/parameters/components/ParameterSettings/ParameterSettings.styled.tsx index 539af9a2e85476531be45b7dfac9f1855ea74ec4..45a70dd51d88ab4cf38bb646cdf69cb892e3618c 100644 --- a/frontend/src/metabase/parameters/components/ParameterSettings/ParameterSettings.styled.tsx +++ b/frontend/src/metabase/parameters/components/ParameterSettings/ParameterSettings.styled.tsx @@ -10,7 +10,6 @@ export const SettingLabel = styled.label` `; export const SettingLabelError = styled.span` - margin: 0 0.5rem; color: var(--mb-color-error); `; diff --git a/frontend/src/metabase/parameters/components/ParameterSettings/ParameterSettings.tsx b/frontend/src/metabase/parameters/components/ParameterSettings/ParameterSettings.tsx index baa387f1cc2bfe5a098fd64f7a0c2c295d82eba0..4bd86fc9cdc76e12b16261123a406f60ebbeb70c 100644 --- a/frontend/src/metabase/parameters/components/ParameterSettings/ParameterSettings.tsx +++ b/frontend/src/metabase/parameters/components/ParameterSettings/ParameterSettings.tsx @@ -236,21 +236,23 @@ export const ParameterSettings = ({ )} <Box mb="lg"> - <SettingLabel> + <SettingLabel id="default-value-label"> {t`Default value`} {parameter.required && parameterHasNoDisplayValue(parameter.default) && ( - <SettingLabelError>({t`required`})</SettingLabelError> + <SettingLabelError> ({t`required`})</SettingLabelError> )} </SettingLabel> - <SettingValueWidget - parameter={parameter} - value={parameter.default} - placeholder={t`No default`} - setValue={onChangeDefaultValue} - mimicMantine - /> + <div aria-labelledby="default-value-label"> + <SettingValueWidget + parameter={parameter} + value={parameter.default} + placeholder={t`No default`} + setValue={onChangeDefaultValue} + mimicMantine + /> + </div> <RequiredParamToggle // This forces the toggle to be a new instance when the parameter changes, diff --git a/frontend/src/metabase/parameters/components/ParameterSettings/TemporalUnitSettings/TemporalUnitSettings.tsx b/frontend/src/metabase/parameters/components/ParameterSettings/TemporalUnitSettings/TemporalUnitSettings.tsx index 15ec667867955fb52d626d826b44bcc1627457a4..b422632f0f7d83d0856f225654d3e633c0118a9c 100644 --- a/frontend/src/metabase/parameters/components/ParameterSettings/TemporalUnitSettings/TemporalUnitSettings.tsx +++ b/frontend/src/metabase/parameters/components/ParameterSettings/TemporalUnitSettings/TemporalUnitSettings.tsx @@ -7,6 +7,7 @@ import { Divider, Icon, Popover, + rem, Text, } from "metabase/ui"; import * as Lib from "metabase-lib"; @@ -37,6 +38,7 @@ export function TemporalUnitSettings({ fw="normal" rightIcon={<Icon name="chevrondown" />} fullWidth + px={rem(11)} // needs to be the same as default input paddingLeft in Input.styled.tsx styles={{ inner: { justifyContent: "space-between" } }} // justify prop in mantine v7 > {getSelectedText(selectedUnits, isAll, isNone)} diff --git a/frontend/src/metabase/parameters/components/ParameterValuePicker/ListPicker/ListPicker.tsx b/frontend/src/metabase/parameters/components/ParameterValuePicker/ListPicker/ListPicker.tsx index 3bfaf3af93f73ec4dc73c2dc965761bcb2490e48..03880edb6ca48af155009cd26279a9d5da2b6c52 100644 --- a/frontend/src/metabase/parameters/components/ParameterValuePicker/ListPicker/ListPicker.tsx +++ b/frontend/src/metabase/parameters/components/ParameterValuePicker/ListPicker/ListPicker.tsx @@ -1,5 +1,6 @@ import { useCallback, useRef } from "react"; import { useUnmount } from "react-use"; +import { t } from "ttag"; import { useDebouncedCallback } from "use-debounce"; import { Select, Loader, type SelectOption } from "metabase/ui"; @@ -49,7 +50,7 @@ export function ListPicker(props: ListPickerProps) { <Loader size="xs" /> </div> ) : value ? ( - <PickerIcon name="close" onClick={onClear} /> + <PickerIcon aria-label={t`Clear`} name="close" onClick={onClear} /> ) : null; const debouncedOnSearch = useDebouncedCallback( diff --git a/frontend/src/metabase/parameters/components/ParameterValuePicker/ListPickerConnected/ListPickerConnected.unit.spec.tsx b/frontend/src/metabase/parameters/components/ParameterValuePicker/ListPickerConnected/ListPickerConnected.unit.spec.tsx index 5f3f54fd5ecf3a415ef6defc94df582039b14162..a55610c7be26c8fb5602859090ea1d2b5c3a1133 100644 --- a/frontend/src/metabase/parameters/components/ParameterValuePicker/ListPickerConnected/ListPickerConnected.unit.spec.tsx +++ b/frontend/src/metabase/parameters/components/ParameterValuePicker/ListPickerConnected/ListPickerConnected.unit.spec.tsx @@ -228,13 +228,13 @@ describe("ListPickerConnected", () => { }); }); - it("clears value when clicked on close", async () => { + it("clears value when clicked on Clear", async () => { const { onChangeMock } = setup({ value: "1-1245 Lee Road 146", parameter: getStaticListParam(), }); - await userEvent.click(screen.getByLabelText("close icon")); + await userEvent.click(screen.getByLabelText("Clear")); expect(onChangeMock).toHaveBeenCalledTimes(1); expect(onChangeMock).toHaveBeenCalledWith(null); onChangeMock.mockClear(); diff --git a/frontend/src/metabase/parameters/components/ParameterValuePicker/OwnDatePicker/OwnDatePicker.tsx b/frontend/src/metabase/parameters/components/ParameterValuePicker/OwnDatePicker/OwnDatePicker.tsx index 8e5dcf57e29ec72803b0fec79b7d016fe2bc2831..12c284c4891975d32f0c59f8150e1b1ce80d93cd 100644 --- a/frontend/src/metabase/parameters/components/ParameterValuePicker/OwnDatePicker/OwnDatePicker.tsx +++ b/frontend/src/metabase/parameters/components/ParameterValuePicker/OwnDatePicker/OwnDatePicker.tsx @@ -1,5 +1,6 @@ import { useClickOutside } from "@mantine/hooks"; import { useState } from "react"; +import { t } from "ttag"; import { DateAllOptionsWidget } from "metabase/components/DateAllOptionsWidget"; import { DateMonthYearWidget } from "metabase/components/DateMonthYearWidget"; @@ -39,6 +40,7 @@ export function OwnDatePicker(props: OwnDatePickerProps) { const icon = value ? ( <PickerIcon + aria-label={t`Clear`} name="close" onClick={() => { onChange(null); diff --git a/frontend/src/metabase/parameters/components/ParameterValuePicker/PlainValueInput/PlainValueInput.tsx b/frontend/src/metabase/parameters/components/ParameterValuePicker/PlainValueInput/PlainValueInput.tsx index e8b3819e7011d8bdef17cc3724fe460c77515a6d..9a3ea146a89b019138f49e511ac2f72f98f682e9 100644 --- a/frontend/src/metabase/parameters/components/ParameterValuePicker/PlainValueInput/PlainValueInput.tsx +++ b/frontend/src/metabase/parameters/components/ParameterValuePicker/PlainValueInput/PlainValueInput.tsx @@ -1,4 +1,5 @@ import type { ChangeEvent } from "react"; +import { t } from "ttag"; import { TextInput } from "metabase/ui"; @@ -23,7 +24,11 @@ export function PlainValueInput(props: PlainValueInputProps) { }; const icon = value ? ( - <PickerIcon name="close" onClick={() => onChange(null)} /> + <PickerIcon + aria-label={t`Clear`} + name="close" + onClick={() => onChange(null)} + /> ) : null; return ( diff --git a/frontend/src/metabase/parameters/components/ParameterValueWidget.module.css b/frontend/src/metabase/parameters/components/ParameterValueWidget.module.css index 1127668608917553c976e4ec7c0692700884c545..14a3ed0e987fab44307c89a7fc95ee1244f1fab3 100644 --- a/frontend/src/metabase/parameters/components/ParameterValueWidget.module.css +++ b/frontend/src/metabase/parameters/components/ParameterValueWidget.module.css @@ -69,3 +69,7 @@ font-weight: bold; color: var(--mb-color-text-secondary); } + +.widgetStatus { + flex: 0 0 auto; +} diff --git a/frontend/src/metabase/parameters/components/ParameterValueWidget.tsx b/frontend/src/metabase/parameters/components/ParameterValueWidget.tsx index 29959d6681cd8e161003fffd1e16bfd3dec46564..920f6deec68d5c6865ded47b5973e0ba56712eef 100644 --- a/frontend/src/metabase/parameters/components/ParameterValueWidget.tsx +++ b/frontend/src/metabase/parameters/components/ParameterValueWidget.tsx @@ -8,7 +8,6 @@ import CS from "metabase/css/core/index.css"; import FormattedParameterValue from "metabase/parameters/components/FormattedParameterValue"; import S from "metabase/parameters/components/ParameterValueWidget.module.css"; import { ParameterValueWidgetTrigger } from "metabase/parameters/components/ParameterValueWidgetTrigger"; -import { WidgetStatusIcon } from "metabase/parameters/components/WidgetStatusIcon"; import { getParameterIconName } from "metabase/parameters/utils/ui"; import { Box, Icon, Popover, type PopoverProps } from "metabase/ui"; import type Question from "metabase-lib/v1/Question"; @@ -22,6 +21,7 @@ import { import type { Dashboard, ParameterId } from "metabase-types/api"; import { ParameterDropdownWidget } from "./ParameterDropdownWidget"; +import { WidgetStatus } from "./WidgetStatus"; export type ParameterValueWidgetProps = { parameter: UiParameter; @@ -68,6 +68,8 @@ export const ParameterValueWidget = ({ const [isFocused, setIsFocused] = useState(false); const hasValue = !parameterHasNoDisplayValue(value); + const hasDefaultValue = !parameterHasNoDisplayValue(parameter.default); + const fieldHasValueOrFocus = parameter.value != null || isFocused; const noPopover = hasNoPopover(parameter); const parameterTypeIcon = getParameterIconName(parameter); const showTypeIcon = !isEditing && !hasValue && !isFocused; @@ -75,10 +77,28 @@ export const ParameterValueWidget = ({ const [isOpen, { close, toggle }] = useDisclosure(); const getOptionalActionIcon = () => { - if (value != null) { + const { default: defaultValue } = parameter; + + if ( + hasDefaultValue && + !areParameterValuesIdentical(wrapArray(value), wrapArray(defaultValue)) + ) { return ( - <WidgetStatusIcon - name="close" + <WidgetStatus + className={S.widgetStatus} + highlighted={fieldHasValueOrFocus} + status="reset" + onClick={() => setParameterValueToDefault?.(parameter.id)} + /> + ); + } + + if (hasValue) { + return ( + <WidgetStatus + className={S.widgetStatus} + highlighted={fieldHasValueOrFocus} + status="clear" onClick={() => { setValue(null); close(); @@ -88,12 +108,7 @@ export const ParameterValueWidget = ({ } if (!hasNoPopover(parameter)) { - return ( - <WidgetStatusIcon - name="chevrondown" - size={mimicMantine ? 16 : undefined} - /> - ); + return <WidgetStatus className={S.widgetStatus} status="empty" />; } }; @@ -102,16 +117,32 @@ export const ParameterValueWidget = ({ if ( required && - defaultValue && + hasDefaultValue && !areParameterValuesIdentical(wrapArray(value), wrapArray(defaultValue)) ) { return ( - <WidgetStatusIcon - name="time_history" + <WidgetStatus + className={S.widgetStatus} + highlighted={fieldHasValueOrFocus} + status="reset" onClick={() => setParameterValueToDefault?.(parameter.id)} /> ); } + + if (required && !hasDefaultValue && hasValue) { + return ( + <WidgetStatus + className={S.widgetStatus} + highlighted={fieldHasValueOrFocus} + status="clear" + onClick={() => { + setValue(null); + close(); + }} + /> + ); + } }; const getActionIcon = () => { @@ -126,7 +157,7 @@ export const ParameterValueWidget = ({ if (!icon) { // This is required to keep input width constant - return <WidgetStatusIcon name="empty" />; + return <WidgetStatus className={S.widgetStatus} status="none" />; } return icon; @@ -135,7 +166,7 @@ export const ParameterValueWidget = ({ const resetToDefault = () => { const { required, default: defaultValue } = parameter; - if (required && defaultValue && !value) { + if (required && defaultValue != null && !value) { setValue(defaultValue); } }; @@ -159,6 +190,7 @@ export const ParameterValueWidget = ({ > <ParameterValueWidgetTrigger className={cx(S.noPopover, className)} + ariaLabel={parameter.name} hasValue={hasValue} > {showTypeIcon && ( diff --git a/frontend/src/metabase/parameters/components/ParameterValueWidgetTrigger.tsx b/frontend/src/metabase/parameters/components/ParameterValueWidgetTrigger.tsx index 9bc7fd653a7c2e8b8584eeaec9f29e519d03c4ea..8d4f5f195d8efa021f29959f36293843b4d11725 100644 --- a/frontend/src/metabase/parameters/components/ParameterValueWidgetTrigger.tsx +++ b/frontend/src/metabase/parameters/components/ParameterValueWidgetTrigger.tsx @@ -26,7 +26,7 @@ function ParameterValueWidgetTriggerInner( ) { if (mimicMantine) { return ( - <TriggerContainer ref={ref} hasValue={hasValue}> + <TriggerContainer aria-label={ariaLabel} ref={ref} hasValue={hasValue}> {children} </TriggerContainer> ); diff --git a/frontend/src/metabase/parameters/components/WidgetStatus/WidgetStatus.tsx b/frontend/src/metabase/parameters/components/WidgetStatus/WidgetStatus.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d381242e6619ae6e62958ea42a53edecd4645770 --- /dev/null +++ b/frontend/src/metabase/parameters/components/WidgetStatus/WidgetStatus.tsx @@ -0,0 +1,78 @@ +import type { MouseEvent } from "react"; +import { t } from "ttag"; + +import { Button, Flex, Icon, rem, Tooltip } from "metabase/ui"; + +import type { Status } from "./types"; + +type Props = { + className?: string; + highlighted?: boolean; + status: Status; + onClick?: () => void; +}; + +const COMPACT_BUTTON_PADDING = 4; + +/** + * Account for compact button's padding, so that the placement of this component's root element + * is the same regardless of whether the button is rendered or not. + */ +const BUTTON_MARGIN = -COMPACT_BUTTON_PADDING; + +export const WidgetStatus = ({ + className, + highlighted, + status, + onClick, +}: Props) => { + const handleClick = (event: MouseEvent) => { + if (onClick) { + event.stopPropagation(); + onClick(); + } + }; + + return ( + <Flex + align="center" + className={className} + h={0} // trick to prevent this element from affecting parent's height + ml="auto" + > + {status === "clear" && ( + <Tooltip label={t`Clear`}> + <Button + aria-label={t`Clear`} + color={highlighted ? undefined : "text-medium"} + compact + leftIcon={<Icon name="close" />} + m={rem(BUTTON_MARGIN)} + radius="md" + variant="subtle" + onClick={handleClick} + /> + </Tooltip> + )} + + {status === "reset" && ( + <Tooltip label={t`Reset filter to default state`}> + <Button + aria-label={t`Reset filter to default state`} + color={highlighted ? undefined : "text-medium"} + compact + leftIcon={<Icon name="revert" />} + m={rem(BUTTON_MARGIN)} + radius="md" + variant="subtle" + onClick={handleClick} + /> + </Tooltip> + )} + + {status === "empty" && <Icon name="chevrondown" />} + + {status === "none" && <Icon name="empty" />} + </Flex> + ); +}; diff --git a/frontend/src/metabase/parameters/components/WidgetStatus/WidgetStatus.unit.spec.tsx b/frontend/src/metabase/parameters/components/WidgetStatus/WidgetStatus.unit.spec.tsx new file mode 100644 index 0000000000000000000000000000000000000000..56e6fe34df2031fa687ec7fc20c0d06523d15782 --- /dev/null +++ b/frontend/src/metabase/parameters/components/WidgetStatus/WidgetStatus.unit.spec.tsx @@ -0,0 +1,114 @@ +import userEvent from "@testing-library/user-event"; + +import { render, screen } from "__support__/ui"; + +import { WidgetStatus } from "./WidgetStatus"; +import type { Status } from "./types"; + +interface SetupOpts { + status: Status; +} + +function setup({ status }: SetupOpts) { + const onClick = jest.fn(); + + render(<WidgetStatus status={status} onClick={onClick} />); + + return { onClick }; +} + +describe("WidgetStatus", () => { + describe("status='clear'", () => { + it("renders correctly", () => { + setup({ status: "clear" }); + + expect(screen.getByLabelText("close icon")).toBeInTheDocument(); + expect(screen.getByRole("button")).toBeEnabled(); + }); + + it("has tooltip", async () => { + setup({ status: "clear" }); + + await userEvent.hover(screen.getByRole("button")); + expect(screen.getByRole("tooltip")).toHaveTextContent("Clear"); + }); + + it("is clickable", async () => { + const { onClick } = setup({ status: "clear" }); + + await userEvent.click(screen.getByRole("button")); + expect(onClick).toHaveBeenCalled(); + }); + }); + + describe("status='reset'", () => { + it("renders correctly", () => { + setup({ status: "reset" }); + + expect(screen.getByLabelText("revert icon")).toBeInTheDocument(); + expect(screen.getByRole("button")).toBeEnabled(); + }); + + it("has tooltip", async () => { + setup({ status: "reset" }); + + await userEvent.hover(screen.getByRole("button")); + expect(screen.getByRole("tooltip")).toHaveTextContent( + "Reset filter to default state", + ); + }); + + it("is clickable", async () => { + const { onClick } = setup({ status: "reset" }); + + await userEvent.click(screen.getByRole("button")); + expect(onClick).toHaveBeenCalled(); + }); + }); + + describe("status='empty'", () => { + it("renders correctly", () => { + setup({ status: "empty" }); + + expect(screen.getByLabelText("chevrondown icon")).toBeInTheDocument(); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); + + it("does not have tooltip", async () => { + setup({ status: "empty" }); + + await userEvent.hover(screen.getByRole("img")); + expect(screen.queryByRole("tooltip")).not.toBeInTheDocument(); + }); + + it("is not clickable", async () => { + const { onClick } = setup({ status: "empty" }); + + await userEvent.click(screen.getByRole("img")); + expect(onClick).not.toHaveBeenCalled(); + }); + }); + + describe("status='none'", () => { + it("renders correctly", () => { + setup({ status: "none" }); + + expect(screen.getByLabelText("empty icon")).toBeInTheDocument(); + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); + + it("does not have tooltip", async () => { + setup({ status: "none" }); + + await userEvent.hover(screen.getByRole("img")); + expect(screen.queryByRole("tooltip")).not.toBeInTheDocument(); + }); + + it("is not clickable", async () => { + const { onClick } = setup({ status: "none" }); + + await userEvent.click(screen.getByRole("img")); + expect(onClick).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/frontend/src/metabase/parameters/components/WidgetStatus/index.ts b/frontend/src/metabase/parameters/components/WidgetStatus/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e7d78439af03c99a8147843c42484fd1c5e892e --- /dev/null +++ b/frontend/src/metabase/parameters/components/WidgetStatus/index.ts @@ -0,0 +1 @@ +export * from "./WidgetStatus"; diff --git a/frontend/src/metabase/parameters/components/WidgetStatus/types.ts b/frontend/src/metabase/parameters/components/WidgetStatus/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..9cd2d9c9fc62f3051c229911561bd933a2f762b1 --- /dev/null +++ b/frontend/src/metabase/parameters/components/WidgetStatus/types.ts @@ -0,0 +1,7 @@ +/** + * clear - to indicate the value can be removed + * reset - to indicate the value can be reset to default + * empty - to indicate the value can be selected + * none - when the component is not needed, but we still render it to preserve space for it + */ +export type Status = "clear" | "reset" | "empty" | "none"; diff --git a/frontend/src/metabase/parameters/components/WidgetStatusIcon.tsx b/frontend/src/metabase/parameters/components/WidgetStatusIcon.tsx deleted file mode 100644 index fec2c24d2b17bfd6e4be5a751fb9491a098545e4..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/parameters/components/WidgetStatusIcon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import cx from "classnames"; -import type { MouseEvent } from "react"; - -import CS from "metabase/css/core/index.css"; -import { Icon } from "metabase/ui"; - -type WidgetStatusIconProps = { - name: "close" | "empty" | "chevrondown" | "time_history"; - onClick?: () => void; - size?: number; -}; - -export function WidgetStatusIcon({ - name, - size = 12, - onClick, -}: WidgetStatusIconProps) { - const classes = cx(CS.flexAlignRight, CS.flexNoShrink, { - [CS.cursorPointer]: ["close", "time_history"].includes(name), - }); - - const handleOnClick = (e: MouseEvent) => { - if (onClick) { - e.stopPropagation(); - onClick(); - } - }; - - return ( - <Icon name={name} onClick={handleOnClick} size={size} className={classes} /> - ); -} diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParamParts/DefaultRequiredValueControl.tsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParamParts/DefaultRequiredValueControl.tsx index d00cfbc12e99383e56b90784e16a2018ab11f0d2..168bf876c9a30709d194165848e6cf67a72bd4b6 100644 --- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParamParts/DefaultRequiredValueControl.tsx +++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParamParts/DefaultRequiredValueControl.tsx @@ -25,18 +25,20 @@ export function DefaultRequiredValueControl({ return ( <div> - <ContainerLabel> + <ContainerLabel id={`default-value-label-${tag.id}`}> {t`Default filter widget value`} - {isMissing && <ErrorSpan>({t`required`})</ErrorSpan>} + {isMissing && <ErrorSpan> ({t`required`})</ErrorSpan>} </ContainerLabel> <Flex gap="xs" direction="column"> - <ParameterValuePicker - tag={tag} - parameter={parameter} - value={tag.default} - onValueChange={onChangeDefaultValue} - /> + <div aria-labelledby={`default-value-label-${tag.id}`}> + <ParameterValuePicker + tag={tag} + parameter={parameter} + value={tag.default} + onValueChange={onChangeDefaultValue} + /> + </div> <RequiredParamToggle uniqueId={tag.id} diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParamParts/TagEditorParam.styled.tsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParamParts/TagEditorParam.styled.tsx index 1014dc4091bed7319156ca2d6524bf5b7d8b4637..c0456fc805b79cd6b0d8aa4c7e169ab0d94b42be 100644 --- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParamParts/TagEditorParam.styled.tsx +++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParamParts/TagEditorParam.styled.tsx @@ -27,7 +27,6 @@ export const ContainerLabel = styled.div<ContainerLabelProps>` `; export const ErrorSpan = styled.span` - margin: 0 0.5rem; color: var(--mb-color-error); `;