diff --git a/frontend/src/metabase-lib/parameters/constants.ts b/frontend/src/metabase-lib/parameters/constants.ts index 015625060d909f36e643fb2a2d5ba7cf677a806e..5c05d0270daa68ff815f6977f7fa225c3aeb70e5 100644 --- a/frontend/src/metabase-lib/parameters/constants.ts +++ b/frontend/src/metabase-lib/parameters/constants.ts @@ -148,11 +148,6 @@ export const LOCATION_OPTIONS = [ }, ]; -export const CUSTOM_SOURCE_PARAMETER_TYPES: Record<string, string[]> = { - string: ["="], - location: ["="], -}; - export const TYPE_SUPPORTS_LINKED_FILTERS = [ "string", "category", diff --git a/frontend/src/metabase-lib/parameters/utils/parameter-source.ts b/frontend/src/metabase-lib/parameters/utils/parameter-source.ts index 3017e4871472d8e3b71b7dc108ee283f1203d20f..b4d621c866182cb297eefec9aed07b65af7960d2 100644 --- a/frontend/src/metabase-lib/parameters/utils/parameter-source.ts +++ b/frontend/src/metabase-lib/parameters/utils/parameter-source.ts @@ -6,8 +6,9 @@ import { ValuesSourceConfig, ValuesSourceType, } from "metabase-types/api"; -import { getFields } from "metabase-lib/parameters/utils/parameter-fields"; import Field from "metabase-lib/metadata/Field"; +import { getFields } from "./parameter-fields"; +import { getParameterSubType, getParameterType } from "./parameter-type"; export const getQueryType = (parameter: Parameter): ValuesQueryType => { return parameter.values_query_type ?? "list"; @@ -21,6 +22,21 @@ export const getSourceConfig = (parameter: Parameter): ValuesSourceConfig => { return parameter.values_source_config ?? {}; }; +export const canUseCustomSource = (parameter: Parameter) => { + const type = getParameterType(parameter); + const subType = getParameterSubType(parameter); + + switch (type) { + case "string": + case "location": + return subType === "="; + case "category": + return true; + default: + return false; + } +}; + export const isValidSourceConfig = ( sourceType: ValuesSourceType, { card_id, value_field, values }: ValuesSourceConfig, diff --git a/frontend/src/metabase-lib/parameters/utils/template-tags.ts b/frontend/src/metabase-lib/parameters/utils/template-tags.ts index 60a687e3dd8d7c243b2d7cc182161e298264c18c..fba053ad9307aaacf4b340b3a47de45c1dd66b70 100644 --- a/frontend/src/metabase-lib/parameters/utils/template-tags.ts +++ b/frontend/src/metabase-lib/parameters/utils/template-tags.ts @@ -38,6 +38,9 @@ export function getTemplateTagParameter(tag: TemplateTag): ParameterWithTarget { name: tag["display-name"], slug: tag.name, default: tag.default, + values_query_type: tag.values_query_type, + values_source_type: tag.values_source_type, + values_source_config: tag.values_source_config, }; } diff --git a/frontend/src/metabase-types/types/Query.ts b/frontend/src/metabase-types/types/Query.ts index 6938fdb58e94200cad4afc4046a161d639f4bd7e..ee5ab520049848be77c1e3d7c46f4714b5417259 100644 --- a/frontend/src/metabase-types/types/Query.ts +++ b/frontend/src/metabase-types/types/Query.ts @@ -3,6 +3,11 @@ * @deprecated use existing types from, or add to metabase-types/api/* */ +import { + ValuesQueryType, + ValuesSourceConfig, + ValuesSourceType, +} from "metabase-types/api"; import { DatetimeUnit } from "metabase-types/api/query"; import { TableId } from "./Table"; import { FieldId, BaseType } from "./Field"; @@ -60,6 +65,11 @@ export type TemplateTag = { // Snippet specific "snippet-id"?: number; "snippet-name"?: string; + + // Values source + values_query_type?: ValuesQueryType; + values_source_type?: ValuesSourceType; + values_source_config?: ValuesSourceConfig; }; export type TemplateTags = { [key: TemplateTagName]: TemplateTag }; diff --git a/frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.tsx b/frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.tsx index 11e4991f971483e799b885bd14fce2fed6a75f81..1af186f8be14f1c2a81b14423ea32300f5e8b281 100644 --- a/frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.tsx +++ b/frontend/src/metabase/parameters/components/ParameterLinkedFilters/ParameterLinkedFilters.tsx @@ -4,8 +4,13 @@ import Toggle from "metabase/core/components/Toggle"; import Fields from "metabase/entities/fields"; import Tables from "metabase/entities/tables"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; -import { Field, FieldId, ParameterId, Table } from "metabase-types/api"; -import { UiParameter } from "metabase-lib/parameters/types"; +import { + Field, + FieldId, + Parameter, + ParameterId, + Table, +} from "metabase-types/api"; import { usableAsLinkedFilter } from "../../utils/linked-filters"; import useFilterFields from "./use-filter-fields"; import { @@ -25,8 +30,8 @@ import { } from "./ParameterLinkedFilters.styled"; export interface ParameterLinkedFiltersProps { - parameter: UiParameter; - otherParameters: UiParameter[]; + parameter: Parameter; + otherParameters: Parameter[]; onChangeFilteringParameters: (filteringParameters: ParameterId[]) => void; onShowAddParameterPopover: () => void; } @@ -50,7 +55,7 @@ const ParameterLinkedFilters = ({ ); const handleFilterChange = useCallback( - (otherParameter: UiParameter, isFiltered: boolean) => { + (otherParameter: Parameter, isFiltered: boolean) => { const newParameters = isFiltered ? filteringParameters.concat(otherParameter.id) : filteringParameters.filter(id => id !== otherParameter.id); @@ -61,7 +66,7 @@ const ParameterLinkedFilters = ({ ); const handleExpandedChange = useCallback( - (otherParameter: UiParameter, isExpanded: boolean) => { + (otherParameter: Parameter, isExpanded: boolean) => { setExpandedParameterId(isExpanded ? otherParameter.id : undefined); }, [], @@ -111,12 +116,12 @@ const ParameterLinkedFilters = ({ }; interface LinkedParameterProps { - parameter: UiParameter; - otherParameter: UiParameter; + parameter: Parameter; + otherParameter: Parameter; isFiltered: boolean; isExpanded: boolean; - onFilterChange: (otherParameter: UiParameter, isFiltered: boolean) => void; - onExpandedChange: (otherParameter: UiParameter, isExpanded: boolean) => void; + onFilterChange: (otherParameter: Parameter, isFiltered: boolean) => void; + onExpandedChange: (otherParameter: Parameter, isExpanded: boolean) => void; } const LinkedParameter = ({ @@ -157,8 +162,8 @@ const LinkedParameter = ({ }; interface LinkedFieldListProps { - parameter: UiParameter; - otherParameter: UiParameter; + parameter: Parameter; + otherParameter: Parameter; } const LinkedFieldList = ({ diff --git a/frontend/src/metabase/parameters/components/ParameterLinkedFilters/use-filter-fields.ts b/frontend/src/metabase/parameters/components/ParameterLinkedFilters/use-filter-fields.ts index 12d4161d58afa3d2759e59e29a4096107d34ae8c..031d7110f788eb9673d4aaa95f48d2150148751a 100644 --- a/frontend/src/metabase/parameters/components/ParameterLinkedFilters/use-filter-fields.ts +++ b/frontend/src/metabase/parameters/components/ParameterLinkedFilters/use-filter-fields.ts @@ -2,8 +2,8 @@ import { useCallback, useState } from "react"; import { t } from "ttag"; import { DashboardApi } from "metabase/services"; import { useOnMount } from "metabase/hooks/use-on-mount"; -import { FieldId } from "metabase-types/api"; -import { UiParameter } from "metabase-lib/parameters/types"; +import { FieldId, Parameter } from "metabase-types/api"; +import { getFields } from "metabase-lib/parameters/utils/parameter-fields"; export interface UseFilterFieldsState { data?: FieldId[][]; @@ -12,14 +12,14 @@ export interface UseFilterFieldsState { } const useFilterFields = ( - parameter: UiParameter, - otherParameter: UiParameter, + parameter: Parameter, + otherParameter: Parameter, ): UseFilterFieldsState => { const [state, setState] = useState<UseFilterFieldsState>({ loading: false }); const handleLoad = useCallback(async () => { - const filtered = getParameterFieldIds(parameter); - const filtering = getParameterFieldIds(otherParameter); + const filtered = getFields(parameter).map(field => field.id); + const filtering = getFields(otherParameter).map(field => field.id); if (!filtered.length || !filtered.length) { const errorParameter = !filtered.length ? parameter : otherParameter; @@ -40,18 +40,10 @@ const useFilterFields = ( return state; }; -const getParameterError = ({ name }: UiParameter) => { +const getParameterError = ({ name }: Parameter) => { return t`To view this, ${name} must be connected to at least one field.`; }; -const getParameterFieldIds = (parameter: UiParameter) => { - if ("fields" in parameter) { - return parameter.fields.map(field => field.id); - } else { - return []; - } -}; - const getParameterMapping = (data: Record<FieldId, FieldId[]>) => { return Object.entries(data).flatMap(([filteredId, filteringIds]) => filteringIds.map(filteringId => [filteringId, parseInt(filteredId, 10)]), diff --git a/frontend/src/metabase/parameters/components/ParameterSettings/ParameterSettings.tsx b/frontend/src/metabase/parameters/components/ParameterSettings/ParameterSettings.tsx index e89a395635e1d404fffae9628569a6d38449494a..52e5d64a1c303d1adab9380eadd57afecbf493d5 100644 --- a/frontend/src/metabase/parameters/components/ParameterSettings/ParameterSettings.tsx +++ b/frontend/src/metabase/parameters/components/ParameterSettings/ParameterSettings.tsx @@ -3,16 +3,14 @@ import { t } from "ttag"; import InputBlurChange from "metabase/components/InputBlurChange"; import Radio from "metabase/core/components/Radio"; import { + Parameter, ValuesQueryType, ValuesSourceConfig, ValuesSourceType, } from "metabase-types/api"; -import { UiParameter } from "metabase-lib/parameters/types"; +import { canUseCustomSource } from "metabase-lib/parameters/utils/parameter-source"; import { getIsMultiSelect } from "../../utils/dashboards"; -import { - canUseCustomSource, - isSingleOrMultiSelectable, -} from "../../utils/parameter-type"; +import { isSingleOrMultiSelectable } from "../../utils/parameter-type"; import ValuesSourceSettings from "../ValuesSourceSettings"; import { SettingLabel, @@ -28,7 +26,7 @@ const MULTI_SELECT_OPTIONS = [ ]; export interface ParameterSettingsProps { - parameter: UiParameter; + parameter: Parameter; onChangeName: (name: string) => void; onChangeDefaultValue: (value: unknown) => void; onChangeIsMultiSelect: (isMultiSelect: boolean) => void; diff --git a/frontend/src/metabase/parameters/components/ParameterSidebar/ParameterSidebar.tsx b/frontend/src/metabase/parameters/components/ParameterSidebar/ParameterSidebar.tsx index fee77458a80ed200a53aeb3f19474be1af9f40fa..0a331660a240e5406b288560df11b02df7bf99f7 100644 --- a/frontend/src/metabase/parameters/components/ParameterSidebar/ParameterSidebar.tsx +++ b/frontend/src/metabase/parameters/components/ParameterSidebar/ParameterSidebar.tsx @@ -3,20 +3,20 @@ import { t } from "ttag"; import Radio from "metabase/core/components/Radio"; import Sidebar from "metabase/dashboard/components/Sidebar"; import { + Parameter, ParameterId, ValuesQueryType, ValuesSourceConfig, ValuesSourceType, } from "metabase-types/api"; -import { UiParameter } from "metabase-lib/parameters/types"; import { canUseLinkedFilters } from "../../utils/linked-filters"; import ParameterSettings from "../ParameterSettings"; import ParameterLinkedFilters from "../ParameterLinkedFilters"; import { SidebarBody, SidebarHeader } from "./ParameterSidebar.styled"; export interface ParameterSidebarProps { - parameter: UiParameter; - otherParameters: UiParameter[]; + parameter: Parameter; + otherParameters: Parameter[]; onChangeName: (parameterId: ParameterId, name: string) => void; onChangeDefaultValue: (parameterId: ParameterId, value: unknown) => void; onChangeIsMultiSelect: ( @@ -151,7 +151,7 @@ const ParameterSidebar = ({ ); }; -const getTabs = (parameter: UiParameter) => { +const getTabs = (parameter: Parameter) => { const tabs = [{ value: "settings", name: t`Settings`, icon: "gear" }]; if (canUseLinkedFilters(parameter)) { diff --git a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceModal.tsx b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceModal.tsx index 789201aac25444395ebe6651ccda25eecc8fd7e3..88f221f2e9d79b2d9c29a8ff0d97b2ae77766b82 100644 --- a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceModal.tsx +++ b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceModal.tsx @@ -1,19 +1,22 @@ import React, { useCallback, useMemo, useState } from "react"; -import { ValuesSourceConfig, ValuesSourceType } from "metabase-types/api"; +import { + Parameter, + ValuesSourceConfig, + ValuesSourceType, +} from "metabase-types/api"; import { getNonVirtualFields } from "metabase-lib/parameters/utils/parameter-fields"; import { getSourceConfig, getSourceConfigForType, getSourceType, } from "metabase-lib/parameters/utils/parameter-source"; -import { UiParameter } from "metabase-lib/parameters/types"; import ValuesSourceTypeModal from "./ValuesSourceTypeModal"; import ValuesSourceCardModal from "./ValuesSourceCardModal"; type ModalStep = "main" | "card"; interface ModalProps { - parameter: UiParameter; + parameter: Parameter; onSubmit: ( sourceType: ValuesSourceType, sourceConfig: ValuesSourceConfig, diff --git a/frontend/src/metabase/parameters/components/ValuesSourceSettings/ValuesSourceSettings.tsx b/frontend/src/metabase/parameters/components/ValuesSourceSettings/ValuesSourceSettings.tsx index 9d9a1cbcd2385cdcc8ad6b579cbf04bf88f3e954..800ca78cd9cfcefbc51fd1a9ada6ed7ffe2cf1f7 100644 --- a/frontend/src/metabase/parameters/components/ValuesSourceSettings/ValuesSourceSettings.tsx +++ b/frontend/src/metabase/parameters/components/ValuesSourceSettings/ValuesSourceSettings.tsx @@ -3,12 +3,12 @@ import { t } from "ttag"; import Radio from "metabase/core/components/Radio/Radio"; import Modal from "metabase/components/Modal"; import { + Parameter, ValuesQueryType, ValuesSourceConfig, ValuesSourceType, } from "metabase-types/api"; import { getQueryType } from "metabase-lib/parameters/utils/parameter-source"; -import { UiParameter } from "metabase-lib/parameters/types"; import ValuesSourceModal from "../ValuesSourceModal"; import { RadioLabelButton, @@ -17,7 +17,7 @@ import { } from "./ValuesSourceSettings.styled"; export interface ValuesSourceSettingsProps { - parameter: UiParameter; + parameter: Parameter; onChangeQueryType: (queryType: ValuesQueryType) => void; onChangeSourceType: (sourceType: ValuesSourceType) => void; onChangeSourceConfig: (sourceConfig: ValuesSourceConfig) => void; diff --git a/frontend/src/metabase/parameters/utils/parameter-type.ts b/frontend/src/metabase/parameters/utils/parameter-type.ts index 715a8e58094c07a01c60e7323c0dac272a789ad1..9d1642edf21bae3d104e2a1044b6563675910540 100644 --- a/frontend/src/metabase/parameters/utils/parameter-type.ts +++ b/frontend/src/metabase/parameters/utils/parameter-type.ts @@ -1,11 +1,8 @@ import { Parameter } from "metabase-types/api"; +import { SINGLE_OR_MULTI_SELECTABLE_TYPES } from "metabase-lib/parameters/constants"; import { - CUSTOM_SOURCE_PARAMETER_TYPES, - SINGLE_OR_MULTI_SELECTABLE_TYPES, -} from "metabase-lib/parameters/constants"; -import { - getParameterType, getParameterSubType, + getParameterType, } from "metabase-lib/parameters/utils/parameter-type"; export function isSingleOrMultiSelectable(parameter: Parameter): boolean { @@ -20,13 +17,3 @@ export function isSingleOrMultiSelectable(parameter: Parameter): boolean { } return SINGLE_OR_MULTI_SELECTABLE_TYPES[type].includes(subType); } - -export const canUseCustomSource = (parameter: Parameter) => { - const type = getParameterType(parameter); - const subType = getParameterSubType(parameter); - - return ( - CUSTOM_SOURCE_PARAMETER_TYPES[type] != null && - CUSTOM_SOURCE_PARAMETER_TYPES[type].includes(subType) - ); -}; diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx index af657d894a3baab1c86b548efd342c69b81f8203..1bc62abffc2d6e940dfeba1f1c201626acbe0d7a 100644 --- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx +++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx @@ -11,6 +11,7 @@ import Toggle from "metabase/core/components/Toggle"; import InputBlurChange from "metabase/components/InputBlurChange"; import Select, { Option } from "metabase/core/components/Select"; +import ValuesSourceSettings from "metabase/parameters/components/ValuesSourceSettings"; import { getParameterOptionsForField } from "metabase/parameters/utils/template-tag-options"; import { fetchField } from "metabase/redux/metadata"; @@ -18,6 +19,8 @@ import { getMetadata } from "metabase/selectors/metadata"; import { SchemaTableAndFieldDataSelector } from "metabase/query_builder/components/DataSelector"; import MetabaseSettings from "metabase/lib/settings"; +import { canUseCustomSource } from "metabase-lib/parameters/utils/parameter-source"; + import { ErrorSpan, TagName, @@ -274,6 +277,24 @@ export class TagEditorParam extends Component { /> </InputContainer> + {parameter && canUseCustomSource(parameter) && ( + <InputContainer> + <ContainerLabel>{t`How should users filter on this variable?`}</ContainerLabel> + <ValuesSourceSettings + parameter={parameter} + onChangeQueryType={value => + this.setParameterAttribute("values_query_type", value) + } + onChangeSourceType={value => + this.setParameterAttribute("values_source_type", value) + } + onChangeSourceConfig={value => + this.setParameterAttribute("values_source_config", value) + } + /> + </InputContainer> + )} + {((tag.type !== "dimension" && tag.required) || tag.type === "dimension" || tag["widget-type"]) && ( diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.unit.spec.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.unit.spec.jsx index 4312dc04fc29fe764d3399e41e6e6180d8551970..6589aa6665f21de6ff02548e2e7d3aaa1ab2b8b2 100644 --- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.unit.spec.jsx +++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.unit.spec.jsx @@ -23,6 +23,7 @@ jest.mock("metabase/query_builder/components/DataSelector", () => ({ })); jest.mock("metabase/entities/schemas", () => ({ + load: () => children => children, Loader: ({ children }) => children(), })); diff --git a/frontend/test/__support__/e2e/helpers/e2e-filter-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-filter-helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..b26faa9c381561228144ab3cfcf273efb0121a79 --- /dev/null +++ b/frontend/test/__support__/e2e/helpers/e2e-filter-helpers.js @@ -0,0 +1,43 @@ +import { + modal, + popover, +} from "__support__/e2e/helpers/e2e-ui-elements-helpers"; + +export function setFilterQuestionSource({ question, field }) { + cy.findByText("Dropdown list").click(); + cy.findByText("Edit").click(); + + modal().within(() => { + cy.findByText("From another model or question").click(); + cy.findByText("Pick a model or question…").click(); + }); + + modal().within(() => { + cy.findByPlaceholderText(/Search for a question/).type(question); + cy.findByText(question).click(); + cy.button("Done").click(); + }); + + modal().within(() => { + cy.findByText("Pick a column…").click(); + }); + + popover().within(() => { + cy.findByText(field).click(); + }); + + modal().within(() => { + cy.button("Done").click(); + }); +} + +export function setFilterListSource({ values }) { + cy.findByText("Dropdown list").click(); + cy.findByText("Edit").click(); + + modal().within(() => { + cy.findByText("Custom list").click(); + cy.findByRole("textbox").clear().type(values.join("\n")); + cy.button("Done").click(); + }); +} diff --git a/frontend/test/__support__/e2e/helpers/e2e-misc-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-misc-helpers.js index c5cfc6d8fa1d7066da6488b332a7130ab62a2e7f..f7101dc03f7c47a2dcbb04aea182ff63efd235f3 100644 --- a/frontend/test/__support__/e2e/helpers/e2e-misc-helpers.js +++ b/frontend/test/__support__/e2e/helpers/e2e-misc-helpers.js @@ -1,3 +1,5 @@ +import { modal } from "__support__/e2e/helpers/e2e-ui-elements-helpers"; + // Find a text field by label text, type it in, then blur the field. // Commonly used in our Admin section as we auto-save settings. export function typeAndBlurUsingLabel(label, value) { @@ -218,3 +220,26 @@ export function interceptIfNotPreviouslyDefined({ method, url, alias } = {}) { cy.intercept(method, url).as(alias); } } + +export function saveQuestion( + name, + { wrapId = false, idAlias = "questionId" } = {}, +) { + cy.intercept("POST", "/api/card").as("saveQuestion"); + cy.findByText("Save").click(); + + modal().within(() => { + cy.findByLabelText("Name").type(name); + cy.button("Save").click(); + }); + + cy.wait("@saveQuestion").then(({ response: { body } }) => { + if (wrapId) { + cy.wrap(body.id).as(idAlias); + } + }); + + modal().within(() => { + cy.button("Not now").click(); + }); +} diff --git a/frontend/test/__support__/e2e/helpers/index.js b/frontend/test/__support__/e2e/helpers/index.js index 8ae57a12d63fcba5cb2bc97de4d20ac0e5697236..ebd82cb8f4f419bdadcb3381cdb80444f1fb3755 100644 --- a/frontend/test/__support__/e2e/helpers/index.js +++ b/frontend/test/__support__/e2e/helpers/index.js @@ -6,6 +6,7 @@ export * from "./e2e-database-metadata-helpers"; export * from "./e2e-qa-databases-helpers"; export * from "./e2e-ad-hoc-question-helpers"; export * from "./e2e-enterprise-helpers"; +export * from "./e2e-filter-helpers"; export * from "./e2e-mock-app-settings-helpers"; export * from "./e2e-notebook-helpers"; export * from "./e2e-cloud-helpers"; diff --git a/frontend/test/metabase/scenarios/dashboard-filters/dashboard-filters-source.cy.spec.js b/frontend/test/metabase/scenarios/dashboard-filters/dashboard-filters-source.cy.spec.js index 111eb4cca9065b97e4fa9314269d2ac7c43af17e..fee8d801e44da9b6b34086a7099557d07f58da7a 100644 --- a/frontend/test/metabase/scenarios/dashboard-filters/dashboard-filters-source.cy.spec.js +++ b/frontend/test/metabase/scenarios/dashboard-filters/dashboard-filters-source.cy.spec.js @@ -1,6 +1,5 @@ import { editDashboard, - modal, popover, restore, saveDashboard, @@ -8,6 +7,8 @@ import { visitDashboard, openQuestionActions, visitQuestion, + setFilterQuestionSource, + setFilterListSource, } from "__support__/e2e/helpers"; import { SAMPLE_DATABASE } from "__support__/e2e/cypress_sample_database"; @@ -22,7 +23,7 @@ const dashboardQuestionDetails = { }; const structuredQuestionDetails = { - name: "Categories", + name: "GUI source", query: { "source-table": PRODUCTS_ID, aggregation: [["count"]], @@ -32,7 +33,7 @@ const structuredQuestionDetails = { }; const nativeQuestionDetails = { - name: "Categories", + name: "SQL source", native: { query: "select distinct CATEGORY from PRODUCTS order by CATEGORY limit 2", }, @@ -47,7 +48,7 @@ describe("scenarios > dashboard > filters", () => { }); it("should be able to use a structured question source", () => { - cy.createQuestion(structuredQuestionDetails); + cy.createQuestion(structuredQuestionDetails, { wrapId: true }); cy.createQuestionAndDashboard({ questionDetails: dashboardQuestionDetails, }).then(({ body: { dashboard_id } }) => { @@ -57,14 +58,16 @@ describe("scenarios > dashboard > filters", () => { editDashboard(); setFilter("Text or Category", "Is"); mapFilterToQuestion(); - editDropdown(); - setupStructuredQuestionSource(); + setFilterQuestionSource({ question: "GUI source", field: "Category" }); saveDashboard(); filterDashboard(); + + cy.get("@questionId").then(visitQuestion); + archiveQuestion(); }); it("should be able to use a native question source", () => { - cy.createNativeQuestion(nativeQuestionDetails); + cy.createNativeQuestion(nativeQuestionDetails, { wrapId: true }); cy.createQuestionAndDashboard({ questionDetails: dashboardQuestionDetails, }).then(({ body: { dashboard_id } }) => { @@ -74,10 +77,12 @@ describe("scenarios > dashboard > filters", () => { editDashboard(); setFilter("Text or Category", "Is"); mapFilterToQuestion(); - editDropdown(); - setupNativeQuestionSource(); + setFilterQuestionSource({ question: "SQL source", field: "CATEGORY" }); saveDashboard(); filterDashboard(); + + cy.get("@questionId").then(visitQuestion); + archiveQuestion(); }); it("should be able to use a static list source", () => { @@ -90,113 +95,12 @@ describe("scenarios > dashboard > filters", () => { editDashboard(); setFilter("Text or Category", "Is"); mapFilterToQuestion(); - editDropdown(); - setupCustomList(); + setFilterListSource({ values: ["Doohickey", "Gadget"] }); saveDashboard(); filterDashboard(); }); - - it("should result in a warning being shown when archiving a question it uses", () => { - cy.intercept("POST", "/api/dashboard/**/query").as("getCardQuery"); - - cy.createQuestion(structuredQuestionDetails, { - wrapId: true, - idAlias: "structuredQuestionId", - }); - cy.createQuestionAndDashboard({ - questionDetails: dashboardQuestionDetails, - }).then(({ body: { dashboard_id } }) => { - visitDashboard(dashboard_id); - }); - - editDashboard(); - setFilter("Text or Category", "Is"); - mapFilterToQuestion(); - editDropdown(); - setupStructuredQuestionSource(); - saveDashboard(); - - cy.intercept("GET", "/api/collection/root/items**").as("getItems"); - - cy.get("@structuredQuestionId").then(question_id => { - visitQuestion(question_id); - openQuestionActions(); - cy.findByTestId("archive-button").click(); - modal().within(() => { - cy.findByText( - "This question will be removed from any dashboards or pulses using it. It will also be removed from the filter that uses it to populate values.", - ); - }); - }); - }); }); -const editDropdown = () => { - cy.findByText("Dropdown list").click(); - cy.findByText("Edit").click(); -}; - -const setupStructuredQuestionSource = () => { - modal().within(() => { - cy.findByText("From another model or question").click(); - cy.findByText("Pick a model or question…").click(); - }); - - modal().within(() => { - cy.findByPlaceholderText(/Search for a question/).type("Categories"); - cy.findByText("Categories").click(); - cy.button("Done").click(); - }); - - modal().within(() => { - cy.findByText("Pick a column…").click(); - }); - - popover().within(() => { - cy.findByText("Category").click(); - }); - - modal().within(() => { - cy.wait("@dataset"); - cy.findByDisplayValue(/Gadget/).should("be.visible"); - cy.button("Done").click(); - }); -}; - -const setupNativeQuestionSource = () => { - modal().within(() => { - cy.findByText("From another model or question").click(); - cy.findByText("Pick a model or question…").click(); - }); - - modal().within(() => { - cy.findByText("Categories").click(); - cy.button("Done").click(); - }); - - modal().within(() => { - cy.findByText("Pick a column…").click(); - }); - - popover().within(() => { - cy.findByText("CATEGORY").click(); - }); - - modal().within(() => { - cy.wait("@dataset"); - cy.findByDisplayValue(/Gadget/).should("be.visible"); - cy.button("Done").click(); - }); -}; - -const setupCustomList = () => { - modal().within(() => { - cy.findByText("Custom list").click(); - cy.findByRole("textbox").clear().type("Doohickey\nGadget"); - cy.button("Done").click(); - }); -}; - const mapFilterToQuestion = () => { cy.findByText("Select…").click(); popover().within(() => cy.findByText("Category").click()); @@ -217,3 +121,11 @@ const filterDashboard = () => { cy.wait("@getCardQuery"); }); }; + +const archiveQuestion = () => { + openQuestionActions(); + cy.findByTestId("archive-button").click(); + cy.findByText( + "This question will be removed from any dashboards or pulses using it. It will also be removed from the filter that uses it to populate values.", + ); +}; diff --git a/frontend/test/metabase/scenarios/native-filters/sql-filters-source.cy.spec.js b/frontend/test/metabase/scenarios/native-filters/sql-filters-source.cy.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..f8dac2d660820843e6b24b7d3c1809a5ef7ff4a0 --- /dev/null +++ b/frontend/test/metabase/scenarios/native-filters/sql-filters-source.cy.spec.js @@ -0,0 +1,57 @@ +import { + openNativeEditor, + restore, + setFilterQuestionSource, + saveQuestion, +} from "__support__/e2e/helpers"; +import { SAMPLE_DATABASE } from "__support__/e2e/cypress_sample_database"; +import * as SQLFilter from "./helpers/e2e-sql-filter-helpers"; +import * as FieldFilter from "./helpers/e2e-field-filter-helpers"; + +const { PRODUCTS_ID, PRODUCTS } = SAMPLE_DATABASE; + +const structuredQuestionDetails = { + name: "GUI source", + query: { + "source-table": PRODUCTS_ID, + aggregation: [["count"]], + breakout: [["field", PRODUCTS.CATEGORY, null]], + filter: ["!=", ["field", PRODUCTS.CATEGORY, null], "Gizmo"], + }, +}; + +const nativeQuestionDetails = { + name: "SQL source", + native: { + query: "select distinct CATEGORY from PRODUCTS order by CATEGORY limit 2", + }, +}; + +describe("scenarios > filters > sql filters > values source", () => { + beforeEach(() => { + restore(); + cy.signInAsAdmin(); + }); + + it("should be able to use a custom source for a text filter", () => { + cy.createQuestion(structuredQuestionDetails); + openNativeEditor(); + + SQLFilter.enterParameterizedQuery("SELECT * FROM products WHERE {{f}}"); + SQLFilter.openTypePickerFromDefaultFilterType(); + setFilterQuestionSource({ question: "GUI source", field: "Category" }); + saveQuestion("SQL filter"); + }); + + it("should be able to use a custom source for a field filter", () => { + cy.createNativeQuestion(nativeQuestionDetails); + openNativeEditor(); + + SQLFilter.enterParameterizedQuery("SELECT * FROM products WHERE {{f}}"); + SQLFilter.openTypePickerFromDefaultFilterType(); + SQLFilter.chooseType("Field Filter"); + FieldFilter.mapTo({ table: "Products", field: "Category" }); + setFilterQuestionSource({ question: "SQL source", field: "CATEGORY" }); + saveQuestion("SQL filter"); + }); +});