From f58c83b3629befeaa181c7e3538d4f43cd0ce5be Mon Sep 17 00:00:00 2001
From: Alexander Polyankin <alexander.polyankin@metabase.com>
Date: Tue, 17 Jan 2023 20:19:24 +0200
Subject: [PATCH] Add values source settings to template tags (#27713)

---
 .../src/metabase-lib/parameters/constants.ts  |   5 -
 .../parameters/utils/parameter-source.ts      |  18 ++-
 .../parameters/utils/template-tags.ts         |   3 +
 frontend/src/metabase-types/types/Query.ts    |  10 ++
 .../ParameterLinkedFilters.tsx                |  29 ++--
 .../use-filter-fields.ts                      |  22 +--
 .../ParameterSettings/ParameterSettings.tsx   |  10 +-
 .../ParameterSidebar/ParameterSidebar.tsx     |   8 +-
 .../ValuesSourceModal/ValuesSourceModal.tsx   |   9 +-
 .../ValuesSourceSettings.tsx                  |   4 +-
 .../parameters/utils/parameter-type.ts        |  17 +--
 .../template_tags/TagEditorParam.jsx          |  21 +++
 .../TagEditorParam.unit.spec.jsx              |   1 +
 .../e2e/helpers/e2e-filter-helpers.js         |  43 ++++++
 .../e2e/helpers/e2e-misc-helpers.js           |  25 ++++
 .../test/__support__/e2e/helpers/index.js     |   1 +
 .../dashboard-filters-source.cy.spec.js       | 134 +++---------------
 .../sql-filters-source.cy.spec.js             |  57 ++++++++
 18 files changed, 243 insertions(+), 174 deletions(-)
 create mode 100644 frontend/test/__support__/e2e/helpers/e2e-filter-helpers.js
 create mode 100644 frontend/test/metabase/scenarios/native-filters/sql-filters-source.cy.spec.js

diff --git a/frontend/src/metabase-lib/parameters/constants.ts b/frontend/src/metabase-lib/parameters/constants.ts
index 015625060d9..5c05d0270da 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 3017e487147..b4d621c8661 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 60a687e3dd8..fba053ad930 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 6938fdb58e9..ee5ab520049 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 11e4991f971..1af186f8be1 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 12d4161d58a..031d7110f78 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 e89a395635e..52e5d64a1c3 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 fee77458a80..0a331660a24 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 789201aac25..88f221f2e9d 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 9d9a1cbcd23..800ca78cd9c 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 715a8e58094..9d1642edf21 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 af657d894a3..1bc62abffc2 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 4312dc04fc2..6589aa6665f 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 00000000000..b26faa9c381
--- /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 c5cfc6d8fa1..f7101dc03f7 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 8ae57a12d63..ebd82cb8f4f 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 111eb4cca90..fee8d801e44 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 00000000000..f8dac2d6608
--- /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");
+  });
+});
-- 
GitLab