From 1c0bb298080670ad69b12864999cdd24655fbeea Mon Sep 17 00:00:00 2001
From: Ryan Laurie <30528226+iethree@users.noreply.github.com>
Date: Tue, 24 May 2022 16:55:19 -0600
Subject: [PATCH] Support Segments in Bulk Filter Modal (#22899)

* support for segment filters in modal
---
 .../lib/queries/StructuredQuery.ts            |  42 ++++++-
 .../core/components/Select/Select.jsx         |   6 +-
 frontend/src/metabase/lib/query/filter.js     |   4 +
 frontend/src/metabase/lib/query/query.js      |   2 +
 .../modals/BulkFilterList/BulkFilterList.tsx  |  61 +++++++++-
 .../BulkFilterModal.styled.tsx                |   7 +-
 .../BulkFilterModal/BulkFilterModal.tsx       |  15 ++-
 .../BulkFilterSelect.styled.tsx               |   5 +
 .../BulkFilterSelect/BulkFilterSelect.tsx     |  79 +++++++++++-
 .../filters/modals/BulkFilterSelect/index.ts  |   2 +-
 .../scenarios/filters/filter-bulk.cy.spec.js  | 113 +++++++++++++++++-
 11 files changed, 313 insertions(+), 23 deletions(-)

diff --git a/frontend/src/metabase-lib/lib/queries/StructuredQuery.ts b/frontend/src/metabase-lib/lib/queries/StructuredQuery.ts
index 202f5e030b6..3b8d9e2a8f7 100644
--- a/frontend/src/metabase-lib/lib/queries/StructuredQuery.ts
+++ b/frontend/src/metabase-lib/lib/queries/StructuredQuery.ts
@@ -33,6 +33,7 @@ import Dimension, {
   ExpressionDimension,
   AggregationDimension,
 } from "metabase-lib/lib/Dimension";
+import { isSegment } from "metabase/lib/query/filter";
 import DimensionOptions from "metabase-lib/lib/DimensionOptions";
 import Segment from "../metadata/Segment";
 import { DatabaseEngine, DatabaseId } from "metabase-types/types/Database";
@@ -50,8 +51,10 @@ import Table from "../metadata/Table";
 import Field from "../metadata/Field";
 import { TYPE } from "metabase/lib/types";
 import { fieldRefForColumn } from "metabase/lib/dataset";
+
 type DimensionFilter = (dimension: Dimension) => boolean;
 type FieldFilter = (filter: Field) => boolean;
+
 export const STRUCTURED_QUERY_TEMPLATE = {
   database: null,
   type: "query",
@@ -63,13 +66,27 @@ export const STRUCTURED_QUERY_TEMPLATE = {
 export interface FilterSection {
   name: string;
   icon: string;
-  items: DimensionOption[];
+  items: (DimensionOption | SegmentOption)[];
 }
 
 export interface DimensionOption {
   dimension: Dimension;
 }
 
+// type guards for determining data types
+export const isSegmentOption = (content: any): content is SegmentOption =>
+  content?.filter && isSegment(content.filter);
+
+export const isDimensionOption = (content: any): content is DimensionOption =>
+  !!content?.dimension;
+
+export interface SegmentOption {
+  name: string;
+  filter: ["segment", number];
+  icon: string;
+  query: StructuredQuery;
+}
+
 /**
  * A wrapper around an MBQL (`query` type @type {DatasetQuery}) object
  */
@@ -849,10 +866,11 @@ class StructuredQueryInner extends AtomicQuery {
   filterFieldOptionSections(
     filter?: (Filter | FilterWrapper) | null | undefined,
     { includeSegments = true } = {},
+    includeAppliedSegments = false,
   ) {
     const filterDimensionOptions = this.filterDimensionOptions();
     const filterSegmentOptions = includeSegments
-      ? this.filterSegmentOptions(filter)
+      ? this.filterSegmentOptions(filter, includeAppliedSegments)
       : [];
     return filterDimensionOptions.sections({
       extraItems: filterSegmentOptions.map(segment => ({
@@ -867,6 +885,7 @@ class StructuredQueryInner extends AtomicQuery {
   topLevelFilterFieldOptionSections(
     filter = null,
     stages = 2,
+    includeAppliedSegments = false,
   ): FilterSection[] {
     const queries = this.queries().slice(-stages);
 
@@ -877,7 +896,9 @@ class StructuredQueryInner extends AtomicQuery {
 
     queries.reverse();
     const sections = [].concat(
-      ...queries.map(q => q.filterFieldOptionSections(filter)),
+      ...queries.map(q =>
+        q.filterFieldOptionSections(filter, undefined, includeAppliedSegments),
+      ),
     );
 
     // special logic to only show aggregation dimensions for post-aggregation dimensions
@@ -913,7 +934,10 @@ class StructuredQueryInner extends AtomicQuery {
   /**
    * @returns @type {Segment}s that can be used as filters.
    */
-  filterSegmentOptions(filter?: Filter | FilterWrapper): Segment[] {
+  filterSegmentOptions(
+    filter?: Filter | FilterWrapper,
+    includeAppliedSegments = false,
+  ): Segment[] {
     if (filter && !(filter instanceof FilterWrapper)) {
       filter = new FilterWrapper(filter, null, this);
     }
@@ -922,7 +946,8 @@ class StructuredQueryInner extends AtomicQuery {
     return this.table().segments.filter(
       segment =>
         (currentSegmentId != null && currentSegmentId === segment.id) ||
-        (!segment.archived && !this.segments().includes(segment)),
+        (!segment.archived &&
+          (includeAppliedSegments || !this.segments().includes(segment))),
     );
   }
 
@@ -974,6 +999,13 @@ class StructuredQueryInner extends AtomicQuery {
     return this._updateQuery(Q.clearFilters, arguments);
   }
 
+  /**
+   * @returns {StructuredQuery} new query with all segment filters removed
+   */
+  clearSegments() {
+    return this._updateQuery(Q.clearSegments, arguments);
+  }
+
   // SORTS
   // TODO: standardize SORT vs ORDER_BY terminology
   sorts(): OrderByWrapper[] {
diff --git a/frontend/src/metabase/core/components/Select/Select.jsx b/frontend/src/metabase/core/components/Select/Select.jsx
index cd0c9e30f64..99119eff386 100644
--- a/frontend/src/metabase/core/components/Select/Select.jsx
+++ b/frontend/src/metabase/core/components/Select/Select.jsx
@@ -41,6 +41,7 @@ class Select extends Component {
 
     // SelectButton props
     buttonProps: PropTypes.object,
+    buttonText: PropTypes.string, // will override selected options text
 
     // AccordianList props
     searchProp: PropTypes.string,
@@ -136,6 +137,7 @@ class Select extends Component {
       value = this.itemIsSelected(option)
         ? values.filter(value => value !== optionValue)
         : [...values, optionValue];
+      value.changedItem = optionValue;
     } else {
       value = optionValue;
     }
@@ -216,7 +218,9 @@ class Select extends Component {
               disabled={disabled}
               {...buttonProps}
             >
-              {selectedNames.length > 0
+              {this.props.buttonText
+                ? this.props.buttonText
+                : selectedNames.length > 0
                 ? selectedNames.map((name, index) => (
                     <span key={index}>
                       {name}
diff --git a/frontend/src/metabase/lib/query/filter.js b/frontend/src/metabase/lib/query/filter.js
index 7fa3733ac1a..a9d5660b36e 100644
--- a/frontend/src/metabase/lib/query/filter.js
+++ b/frontend/src/metabase/lib/query/filter.js
@@ -45,6 +45,10 @@ export function removeFilter(filter, index) {
 export function clearFilters(filter) {
   return getFilterClause(clear());
 }
+export function clearSegments(filters) {
+  const newFilters = filters.filter(f => !isSegment(f));
+  return getFilterClause(newFilters);
+}
 
 // MISC
 
diff --git a/frontend/src/metabase/lib/query/query.js b/frontend/src/metabase/lib/query/query.js
index 85931dae778..e7e9d98ea70 100644
--- a/frontend/src/metabase/lib/query/query.js
+++ b/frontend/src/metabase/lib/query/query.js
@@ -52,6 +52,8 @@ export const removeFilter = (query, index) =>
   setFilterClause(query, F.removeFilter(query.filter, index));
 export const clearFilters = query =>
   setFilterClause(query, F.clearFilters(query.filter));
+export const clearSegments = query =>
+  setFilterClause(query, F.clearSegments(getFilters(query)));
 
 export const canAddFilter = query => F.canAddFilter(query.filter);
 
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.tsx b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.tsx
index 791b33b234a..fc0820cdcfa 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.tsx
@@ -1,10 +1,17 @@
 import React, { useMemo } from "react";
+import { t } from "ttag";
+
 import StructuredQuery, {
   DimensionOption,
+  SegmentOption,
+  isDimensionOption,
+  isSegmentOption,
 } from "metabase-lib/lib/queries/StructuredQuery";
 import Dimension from "metabase-lib/lib/Dimension";
+import { isSegment } from "metabase/lib/query/filter";
+import { ModalDivider } from "../BulkFilterModal/BulkFilterModal.styled";
 import Filter from "metabase-lib/lib/queries/structured/Filter";
-import BulkFilterSelect from "../BulkFilterSelect";
+import { BulkFilterSelect, SegmentFilterSelect } from "../BulkFilterSelect";
 import {
   ListRoot,
   ListRow,
@@ -15,10 +22,11 @@ import {
 export interface BulkFilterListProps {
   query: StructuredQuery;
   filters: Filter[];
-  options: DimensionOption[];
+  options: (DimensionOption | SegmentOption)[];
   onAddFilter: (filter: Filter) => void;
   onChangeFilter: (filter: Filter, newFilter: Filter) => void;
   onRemoveFilter: (filter: Filter) => void;
+  onClearSegments: () => void;
 }
 
 const BulkFilterList = ({
@@ -28,10 +36,25 @@ const BulkFilterList = ({
   onAddFilter,
   onChangeFilter,
   onRemoveFilter,
+  onClearSegments,
 }: BulkFilterListProps): JSX.Element => {
+  const [dimensions, segments] = useMemo(
+    () => [options.filter(isDimensionOption), options.filter(isSegmentOption)],
+    [options],
+  );
+
   return (
     <ListRoot>
-      {options.map(({ dimension }, index) => (
+      {!!segments.length && (
+        <SegmentListItem
+          query={query}
+          segments={segments}
+          onAddFilter={onAddFilter}
+          onRemoveFilter={onRemoveFilter}
+          onClearSegments={onClearSegments}
+        />
+      )}
+      {dimensions.map(({ dimension }, index) => (
         <BulkFilterListItem
           key={index}
           query={query}
@@ -96,4 +119,36 @@ const BulkFilterListItem = ({
   );
 };
 
+interface SegmentListItemProps {
+  query: StructuredQuery;
+  segments: SegmentOption[];
+  onAddFilter: (filter: Filter) => void;
+  onRemoveFilter: (filter: Filter) => void;
+  onClearSegments: () => void;
+}
+
+const SegmentListItem = ({
+  query,
+  segments,
+  onAddFilter,
+  onRemoveFilter,
+  onClearSegments,
+}: SegmentListItemProps): JSX.Element => (
+  <>
+    <ListRow>
+      <ListRowLabel>{t`Segments`}</ListRowLabel>
+      <ListRowContent>
+        <SegmentFilterSelect
+          query={query}
+          segments={segments}
+          onAddFilter={onAddFilter}
+          onRemoveFilter={onRemoveFilter}
+          onClearSegments={onClearSegments}
+        />
+      </ListRowContent>
+    </ListRow>
+    <ModalDivider marginY="0.5rem" />
+  </>
+);
+
 export default BulkFilterList;
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.styled.tsx b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.styled.tsx
index 973d885bc53..561a276b4cd 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.styled.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.styled.tsx
@@ -47,8 +47,13 @@ export const ModalTabPanel = styled(TabPanel)`
   padding: 0 2rem;
 `;
 
-export const ModalDivider = styled.div`
+interface ModalDividerProps {
+  marginY?: string;
+}
+
+export const ModalDivider = styled.div<ModalDividerProps>`
   border-top: 1px solid ${color("border")};
+  margin: ${props => (props.marginY ? props.marginY : "0")} 0;
 `;
 
 export const ModalCloseButton = styled(IconButtonWrapper)`
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx
index eda72d5ea35..61ed4f69f9e 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx
@@ -39,7 +39,7 @@ const BulkFilterModal = ({
   }, [query]);
 
   const sections = useMemo(() => {
-    return query.topLevelFilterFieldOptionSections();
+    return query.topLevelFilterFieldOptionSections(null, 2, true);
   }, [query]);
 
   const handleAddFilter = useCallback((filter: Filter) => {
@@ -60,6 +60,11 @@ const BulkFilterModal = ({
     setIsChanged(true);
   }, []);
 
+  const handleClearSegments = useCallback(() => {
+    setQuery(query.clearSegments());
+    setIsChanged(true);
+  }, [query]);
+
   const handleApplyQuery = useCallback(() => {
     query.update(undefined, { run: true });
     onClose?.();
@@ -81,6 +86,7 @@ const BulkFilterModal = ({
           onAddFilter={handleAddFilter}
           onChangeFilter={handleChangeFilter}
           onRemoveFilter={handleRemoveFilter}
+          onClearSegments={handleClearSegments}
         />
       ) : (
         <BulkFilterModalSectionList
@@ -90,6 +96,7 @@ const BulkFilterModal = ({
           onAddFilter={handleAddFilter}
           onChangeFilter={handleChangeFilter}
           onRemoveFilter={handleRemoveFilter}
+          onClearSegments={handleClearSegments}
         />
       )}
       <ModalDivider />
@@ -112,6 +119,7 @@ interface BulkFilterModalSectionProps {
   onAddFilter: (filter: Filter) => void;
   onChangeFilter: (filter: Filter, newFilter: Filter) => void;
   onRemoveFilter: (filter: Filter) => void;
+  onClearSegments: () => void;
 }
 
 const BulkFilterModalSection = ({
@@ -121,6 +129,7 @@ const BulkFilterModalSection = ({
   onAddFilter,
   onChangeFilter,
   onRemoveFilter,
+  onClearSegments,
 }: BulkFilterModalSectionProps): JSX.Element => {
   return (
     <ModalBody>
@@ -131,6 +140,7 @@ const BulkFilterModalSection = ({
         onAddFilter={onAddFilter}
         onChangeFilter={onChangeFilter}
         onRemoveFilter={onRemoveFilter}
+        onClearSegments={onClearSegments}
       />
     </ModalBody>
   );
@@ -143,6 +153,7 @@ interface BulkFilterModalSectionListProps {
   onAddFilter: (filter: Filter) => void;
   onChangeFilter: (filter: Filter, newFilter: Filter) => void;
   onRemoveFilter: (filter: Filter) => void;
+  onClearSegments: () => void;
 }
 
 const BulkFilterModalSectionList = ({
@@ -152,6 +163,7 @@ const BulkFilterModalSectionList = ({
   onAddFilter,
   onChangeFilter,
   onRemoveFilter,
+  onClearSegments,
 }: BulkFilterModalSectionListProps): JSX.Element => {
   const [tab, setTab] = useState(0);
 
@@ -178,6 +190,7 @@ const BulkFilterModalSectionList = ({
             onAddFilter={onAddFilter}
             onChangeFilter={onChangeFilter}
             onRemoveFilter={onRemoveFilter}
+            onClearSegments={onClearSegments}
           />
         </ModalTabPanel>
       ))}
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterSelect/BulkFilterSelect.styled.tsx b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterSelect/BulkFilterSelect.styled.tsx
index a89394bab08..a670e83e8cc 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterSelect/BulkFilterSelect.styled.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterSelect/BulkFilterSelect.styled.tsx
@@ -1,6 +1,7 @@
 import styled from "@emotion/styled";
 import SelectButton from "metabase/core/components/SelectButton";
 import FilterPopover from "../../FilterPopover";
+import Select from "metabase/core/components/Select";
 
 export const SelectFilterButton = styled(SelectButton)`
   min-height: 2.25rem;
@@ -10,6 +11,10 @@ export const SelectFilterButton = styled(SelectButton)`
   }
 `;
 
+export const SegmentSelect = styled(Select)`
+  min-height: 2.25rem;
+`;
+
 export const SelectFilterPopover = styled(FilterPopover)`
   overflow-y: auto;
 `;
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterSelect/BulkFilterSelect.tsx b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterSelect/BulkFilterSelect.tsx
index 166f97a64fe..4fcb1b42cfc 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterSelect/BulkFilterSelect.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterSelect/BulkFilterSelect.tsx
@@ -1,11 +1,17 @@
 import React, { useCallback, useMemo } from "react";
-import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
+import { xor } from "lodash";
+
+import StructuredQuery, {
+  SegmentOption,
+} from "metabase-lib/lib/queries/StructuredQuery";
+
 import Filter from "metabase-lib/lib/queries/structured/Filter";
-import Dimension, { FieldDimension } from "metabase-lib/lib/Dimension";
+import Dimension from "metabase-lib/lib/Dimension";
 import TippyPopoverWithTrigger from "metabase/components/PopoverWithTrigger/TippyPopoverWithTrigger";
 import {
   SelectFilterButton,
   SelectFilterPopover,
+  SegmentSelect,
 } from "./BulkFilterSelect.styled";
 
 export interface BulkFilterSelectProps {
@@ -17,7 +23,7 @@ export interface BulkFilterSelectProps {
   onRemoveFilter: (filter: Filter) => void;
 }
 
-const BulkFilterSelect = ({
+export const BulkFilterSelect = ({
   query,
   filter,
   dimension,
@@ -79,9 +85,72 @@ const BulkFilterSelect = ({
   );
 };
 
-const getNewFilter = (query: StructuredQuery, dimension: Dimension) => {
+const getNewFilter = (query: StructuredQuery, dimension: Dimension): Filter => {
   const filter = new Filter([], null, dimension.query() ?? query);
   return filter.setDimension(dimension.mbql(), { useDefaultOperator: true });
 };
 
-export default BulkFilterSelect;
+export interface SegmentFilterSelectProps {
+  query: StructuredQuery;
+  segments: SegmentOption[];
+  onAddFilter: (filter: Filter) => void;
+  onRemoveFilter: (filter: Filter) => void;
+  onClearSegments: () => void;
+}
+
+export const SegmentFilterSelect = ({
+  query,
+  segments,
+  onAddFilter,
+  onRemoveFilter,
+  onClearSegments,
+}: SegmentFilterSelectProps): JSX.Element => {
+  const activeSegmentOptions = useMemo(() => {
+    const activeSegmentIds = query.segments().map(s => s.id);
+    return segments.filter(segment =>
+      activeSegmentIds.includes(segment.filter[1]),
+    );
+  }, [query, segments]);
+
+  const toggleSegment = useCallback(
+    (changedSegment: SegmentOption) => {
+      const segmentIsActive = activeSegmentOptions.includes(changedSegment);
+
+      const segmentFilter = segmentIsActive
+        ? (query
+            .filters()
+            .find(
+              f => f[0] === "segment" && f[1] === changedSegment.filter[1],
+            ) as Filter)
+        : new Filter(changedSegment.filter, null, query);
+
+      segmentIsActive
+        ? onRemoveFilter(segmentFilter)
+        : onAddFilter(segmentFilter);
+    },
+    [query, activeSegmentOptions, onRemoveFilter, onAddFilter],
+  );
+
+  return (
+    <SegmentSelect
+      options={segments.map(segment => ({
+        name: segment.name,
+        value: segment,
+        icon: segment.icon,
+      }))}
+      value={activeSegmentOptions}
+      onChange={(e: any) => toggleSegment(e.target.value.changedItem)}
+      multiple
+      buttonProps={{
+        hasValue: activeSegmentOptions.length > 0,
+        highlighted: true,
+        onClear: onClearSegments,
+      }}
+      buttonText={
+        activeSegmentOptions.length > 1
+          ? `${activeSegmentOptions.length} segments`
+          : null
+      }
+    />
+  );
+};
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterSelect/index.ts b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterSelect/index.ts
index 29ea47b7a8d..7d49974d64f 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterSelect/index.ts
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterSelect/index.ts
@@ -1 +1 @@
-export { default } from "./BulkFilterSelect";
+export * from "./BulkFilterSelect";
diff --git a/frontend/test/metabase/scenarios/filters/filter-bulk.cy.spec.js b/frontend/test/metabase/scenarios/filters/filter-bulk.cy.spec.js
index add09525531..c0537910b72 100644
--- a/frontend/test/metabase/scenarios/filters/filter-bulk.cy.spec.js
+++ b/frontend/test/metabase/scenarios/filters/filter-bulk.cy.spec.js
@@ -1,4 +1,9 @@
-import { popover, restore, visitQuestionAdhoc } from "__support__/e2e/cypress";
+import {
+  popover,
+  restore,
+  visitQuestionAdhoc,
+  filter,
+} from "__support__/e2e/cypress";
 import { SAMPLE_DB_ID } from "__support__/e2e/cypress_data";
 import { SAMPLE_DATABASE } from "__support__/e2e/cypress_sample_database";
 
@@ -41,6 +46,10 @@ const aggregatedQuestionDetails = {
   },
 };
 
+const openFilterModal = () => {
+  cy.findByLabelText("Show more filters").click();
+};
+
 describe("scenarios > filters > bulk filtering", () => {
   beforeEach(() => {
     restore();
@@ -49,7 +58,7 @@ describe("scenarios > filters > bulk filtering", () => {
 
   it("should add a filter for a raw query", () => {
     visitQuestionAdhoc(rawQuestionDetails);
-    cy.findByLabelText("Show more filters").click();
+    openFilterModal();
 
     modal().within(() => {
       cy.findByLabelText("Quantity").click();
@@ -72,7 +81,7 @@ describe("scenarios > filters > bulk filtering", () => {
 
   it("should add a filter for an aggregated query", () => {
     visitQuestionAdhoc(aggregatedQuestionDetails);
-    cy.findByLabelText("Show more filters").click();
+    openFilterModal();
 
     modal().within(() => {
       cy.findByLabelText("Count").click();
@@ -102,7 +111,7 @@ describe("scenarios > filters > bulk filtering", () => {
 
   it("should add a filter for linked tables", () => {
     visitQuestionAdhoc(rawQuestionDetails);
-    cy.findByLabelText("Show more filters").click();
+    openFilterModal();
 
     modal().within(() => {
       cy.findByText("Product").click();
@@ -125,7 +134,7 @@ describe("scenarios > filters > bulk filtering", () => {
 
   it("should update an existing filter", () => {
     visitQuestionAdhoc(filteredQuestionDetails);
-    cy.findByLabelText("Show more filters").click();
+    openFilterModal();
 
     modal().within(() => {
       cy.findByText("is less than 30").click();
@@ -153,7 +162,7 @@ describe("scenarios > filters > bulk filtering", () => {
 
   it("should remove an existing filter", () => {
     visitQuestionAdhoc(filteredQuestionDetails);
-    cy.findByLabelText("Show more filters").click();
+    openFilterModal();
 
     modal().within(() => {
       cy.findByText("is less than 30")
@@ -168,6 +177,98 @@ describe("scenarios > filters > bulk filtering", () => {
     cy.findByText("Quantity is less than 30").should("not.exist");
     cy.findByText("Showing 138 rows").should("be.visible");
   });
+
+  describe("segment filters", () => {
+    const SEGMENT_1_NAME = "Orders < 100";
+    const SEGMENT_2_NAME = "Discounted Orders";
+
+    beforeEach(() => {
+      cy.request("POST", "/api/segment", {
+        name: SEGMENT_1_NAME,
+        description: "All orders with a total under $100.",
+        table_id: ORDERS_ID,
+        definition: {
+          "source-table": ORDERS_ID,
+          aggregation: [["count"]],
+          filter: ["<", ["field", ORDERS.TOTAL, null], 100],
+        },
+      });
+
+      cy.request("POST", "/api/segment", {
+        name: SEGMENT_2_NAME,
+        description: "All orders with a discount",
+        table_id: ORDERS_ID,
+        definition: {
+          "source-table": ORDERS_ID,
+          aggregation: [["count"]],
+          filter: [">", ["field", ORDERS.DISCOUNT, null], 0],
+        },
+      });
+    });
+
+    it("should apply and remove segment filter", () => {
+      visitQuestionAdhoc(rawQuestionDetails);
+      openFilterModal();
+
+      modal().within(() => {
+        cy.findByText("Segments")
+          .parent()
+          .within(() => cy.get("button").click());
+      });
+
+      popover().within(() => {
+        cy.findByText(SEGMENT_1_NAME);
+        cy.findByText(SEGMENT_2_NAME).click();
+      });
+
+      modal().within(() => {
+        cy.button("Apply").click();
+        cy.wait("@dataset");
+      });
+
+      cy.findByTestId("qb-filters-panel").findByText(SEGMENT_2_NAME);
+      cy.findByText("Showing 1,915 rows");
+
+      openFilterModal();
+
+      modal().within(() => {
+        cy.findByText("Segments")
+          .parent()
+          .within(() => cy.get("button").click());
+      });
+
+      popover().within(() => {
+        cy.findByText(SEGMENT_2_NAME).click();
+      });
+
+      modal().within(() => {
+        cy.button("Apply").click();
+        cy.wait("@dataset");
+      });
+
+      cy.findByTestId("qb-filters-panel").should("not.exist");
+      cy.findByText("Showing first 2,000 rows");
+    });
+
+    it("should load already applied segments", () => {
+      visitQuestionAdhoc(rawQuestionDetails);
+      filter();
+      cy.findByText(SEGMENT_1_NAME).click();
+
+      cy.findByTestId("qb-filters-panel").findByText(SEGMENT_1_NAME);
+
+      openFilterModal();
+
+      modal().within(() => {
+        cy.findByText("Segments")
+          .parent()
+          .within(() => {
+            cy.findByText(SEGMENT_1_NAME);
+            cy.findByText(SEGMENT_2_NAME).should("not.exist");
+          });
+      });
+    });
+  });
 });
 
 const modal = () => {
-- 
GitLab