From 0bca54d3c99786ac881a52d3d3f2a80871e29a58 Mon Sep 17 00:00:00 2001 From: anna Date: Fri, 6 Nov 2026 14:05:07 -0600 Subject: [PATCH 1/7] refactor: move filter and column button, add current filter cells --- .../components/columns-button/index.js | 2 - .../components/toolbar/index.js | 41 +++++++++------- .../mui/bulk-edit-data-grid/index.js | 6 ++- .../__tests__/helpers.test.js | 46 +++++++++++++++++- .../events/summit-event-list-page/helpers.js | 29 ++++++++++++ .../events/summit-event-list-page/index.js | 47 ++++++++++++++++--- 6 files changed, 142 insertions(+), 29 deletions(-) diff --git a/src/components/mui/bulk-edit-data-grid/components/columns-button/index.js b/src/components/mui/bulk-edit-data-grid/components/columns-button/index.js index 2a62e039e..49b9fb364 100644 --- a/src/components/mui/bulk-edit-data-grid/components/columns-button/index.js +++ b/src/components/mui/bulk-edit-data-grid/components/columns-button/index.js @@ -20,7 +20,6 @@ import { useGridApiContext, useGridSelector } from "@mui/x-data-grid"; -import { ICON_ONLY_BUTTON_SX } from "../../constants"; // clicking the button while its panel is open should close it, but React 16 // runs the button's handlers on document, next to the panel's click-away @@ -40,7 +39,6 @@ const ColumnsButton = ({ buttonRef }) => { ref={buttonRef} slotProps={{ button: { - sx: ICON_ONLY_BUTTON_SX, onPointerDown: () => { wasOpenRef.current = preferencePanel.open && diff --git a/src/components/mui/bulk-edit-data-grid/components/toolbar/index.js b/src/components/mui/bulk-edit-data-grid/components/toolbar/index.js index a7718bf07..b53bac2af 100644 --- a/src/components/mui/bulk-edit-data-grid/components/toolbar/index.js +++ b/src/components/mui/bulk-edit-data-grid/components/toolbar/index.js @@ -30,6 +30,7 @@ const Toolbar = ({ importItems, importLabel, filter, + appliedFilters, onExport, searchProps, editEnabled, @@ -69,6 +70,9 @@ const Toolbar = ({ {addLabel} )} + {onAdd && (importItems.length > 0 || onExport) && ( + + )} {importItems.length > 0 && ( )} - {(onAdd || importItems.length > 0) && ( - - )} - - {filter} {onExport && ( - <> - - - - )} - {onAdd && (importItems.length > 0 || onExport) && ( - - )} {importItems.length > 0 && ( )} - {searchProps && ( - <> - {(onAdd || importItems.length > 0 || onExport) && ( - - )} - - + {onAdd && ( + )} - {filter} - {appliedFilters} + {filter && + cloneElement(filter, { + iconSx: { ...FILTER_ICON_SX, ...filter.props.iconSx } + })} + + {searchProps && ( + + + + )} + {appliedFilters && {appliedFilters}} ); diff --git a/src/components/mui/bulk-edit-data-grid/constants.js b/src/components/mui/bulk-edit-data-grid/constants.js index 903db1a75..3a90a55c0 100644 --- a/src/components/mui/bulk-edit-data-grid/constants.js +++ b/src/components/mui/bulk-edit-data-grid/constants.js @@ -11,11 +11,29 @@ * limitations under the License. * */ +import { alpha } from "@mui/material"; + // icon-only toolbar buttons; the label stays as tooltip/aria-label export const ICON_ONLY_BUTTON_SX = { minWidth: 0, + // the theme's 16px small-button padding is too wide around a lone icon + px: 1, "& .MuiButton-startIcon": { m: 0 } }; +// uicore GridFilter's IconButton, sized and colored like the buttons above +export const FILTER_ICON_SX = { + borderRadius: 1, + // the theme's small-button padding with px: 1, around a small button's icon + py: "9px", + px: 1, + "& svg": { fontSize: 18 }, + color: "primary.main", + "&:hover": { + bgcolor: (theme) => + alpha(theme.palette.primary.main, theme.palette.action.hoverOpacity) + } +}; + // GridActionsCellItem forces small icons; match uicore table's medium (22px) export const ACTION_ICON_SX = { fontSize: "1.375rem" }; diff --git a/src/components/mui/bulk-edit-data-grid/index.js b/src/components/mui/bulk-edit-data-grid/index.js index 7be813a97..b88387b47 100644 --- a/src/components/mui/bulk-edit-data-grid/index.js +++ b/src/components/mui/bulk-edit-data-grid/index.js @@ -327,6 +327,7 @@ const BulkEditDataGrid = ({ onSortModelChange={handleSortModelChange} disableColumnSorting={editEnabled} localeText={{ + toolbarColumns: "", ...(noRowsLabel && { noRowsLabel }) }} hideFooter @@ -431,9 +432,9 @@ BulkEditDataGrid.propTypes = { ), // tooltip / aria-label of the import icon importLabel: PropTypes.string, - // rendered in the grid toolbar, e.g. a uicore + // a uicore ; its iconSx is styled to match the toolbar filter: PropTypes.node, - // shown after the columns and filter buttons, e.g. applied filter chips + // own row under the toolbar, e.g. chips for the applied filters appliedFilters: PropTypes.node, onExport: PropTypes.func, // same shape as GridToolbar's searchProps From e41616f3e37534d99b162a990715a043d1f49b40 Mon Sep 17 00:00:00 2001 From: anna Date: Tue, 10 Nov 2026 11:20:01 -0600 Subject: [PATCH 3/7] fix: update tests --- .../mui/__tests__/bulk-edit-data-grid.test.js | 31 +------------------ .../__tests__/summit-event-list-page.test.js | 1 + 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/src/components/mui/__tests__/bulk-edit-data-grid.test.js b/src/components/mui/__tests__/bulk-edit-data-grid.test.js index c89aa0625..bdf0d187c 100644 --- a/src/components/mui/__tests__/bulk-edit-data-grid.test.js +++ b/src/components/mui/__tests__/bulk-edit-data-grid.test.js @@ -228,10 +228,7 @@ describe("BulkEditDataGrid", () => { const onSearch = jest.fn(); renderGrid({ searchProps: { term: "", onSearch } }); - await userEvent.click( - screen.getByRole("button", { name: "general.search" }) - ); - // opening the search focuses its input + await userEvent.click(screen.getByRole("textbox")); await userEvent.keyboard("keynote"); expect(onSearch).not.toHaveBeenCalled(); @@ -239,32 +236,6 @@ describe("BulkEditDataGrid", () => { expect(onSearch).toHaveBeenCalledWith("keynote"); }); - test("keeps the search box open while a search term is applied, even when cleared", async () => { - renderGrid({ searchProps: { term: "keynote", onSearch: jest.fn() } }); - - const searchbox = screen.getByRole("textbox"); - expect(searchbox).toHaveValue("keynote"); - - await userEvent.clear(searchbox); - await userEvent.tab(); - - expect(screen.getByRole("textbox")).toBeInTheDocument(); - }); - - test("collapses the search box when left empty with no term applied", async () => { - renderGrid({ searchProps: { term: "", onSearch: jest.fn() } }); - - await userEvent.click( - screen.getByRole("button", { name: "general.search" }) - ); - await userEvent.tab(); - - expect(screen.queryByRole("textbox")).not.toBeInTheDocument(); - expect( - screen.getByRole("button", { name: "general.search" }) - ).toBeInTheDocument(); - }); - test("adds from the add button and lists every import option under the import icon", async () => { const onAdd = jest.fn(); const onMuxImport = jest.fn(); diff --git a/src/pages/events/__tests__/summit-event-list-page.test.js b/src/pages/events/__tests__/summit-event-list-page.test.js index 7218670b3..777d780e9 100644 --- a/src/pages/events/__tests__/summit-event-list-page.test.js +++ b/src/pages/events/__tests__/summit-event-list-page.test.js @@ -106,6 +106,7 @@ const mockGridFilterState = { parsedFilter: [], resetFilters: jest.fn(), filterValues: [], + valuesWithIds: [], setFilters: jest.fn(), joinOperator: undefined }; From 40909e91b9bbb86bd7b312330ac566523a03ada5 Mon Sep 17 00:00:00 2001 From: anna Date: Tue, 10 Nov 2026 11:35:12 -0600 Subject: [PATCH 4/7] fix: add filter chip remove tests --- .../__tests__/summit-event-list-page.test.js | 67 ++++++++++++++++++- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/src/pages/events/__tests__/summit-event-list-page.test.js b/src/pages/events/__tests__/summit-event-list-page.test.js index 777d780e9..a5dc53e99 100644 --- a/src/pages/events/__tests__/summit-event-list-page.test.js +++ b/src/pages/events/__tests__/summit-event-list-page.test.js @@ -1,5 +1,5 @@ import React from "react"; -import { screen, act } from "@testing-library/react"; +import { screen, act, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; import flushPromises from "flush-promises"; @@ -32,8 +32,14 @@ jest.mock( () => function BulkEditDataGridMock(props) { mockEditableTableSpy(props); - // the saved views dropdown is rendered inside the grid toolbar - return props.views; + // saved views and applied filter chips render inside the grid toolbar + const { views, appliedFilters } = props; + return ( + <> + {views} + {appliedFilters} + + ); } ); @@ -134,6 +140,7 @@ describe("SummitEventListPage", () => { ); mockGridFilterState.parsedFilter = []; mockGridFilterState.filterValues = []; + mockGridFilterState.valuesWithIds = []; mockGridFilterState.joinOperator = undefined; saveFilterCriteria.mockReturnValue(() => Promise.resolve({ response: { id: 1, name: "view", criteria: [] } }) @@ -450,6 +457,60 @@ describe("SummitEventListPage", () => { ]); }); + test("removing a filter chip keeps the other filters and the join operator", async () => { + const roomA = { criteria: "location_id", operator: "==", value: [2] }; + const roomB = { criteria: "location_id", operator: "==", value: [7] }; + mockGridFilterState.filterValues = [roomA, roomB]; + mockGridFilterState.valuesWithIds = [ + { ...roomA, id: "location_id-0" }, + { ...roomB, id: "location_id-1" } + ]; + mockGridFilterState.joinOperator = "ANY"; + + renderWithRedux(, { + initialState: { + currentSummitState: { + currentSummit: { + id: 12, + time_zone: { name: "UTC" }, + time_zone_id: "UTC", + selection_plans: [], + tracks: [], + event_types: [], + locations: [ + { id: 2, name: "Room A" }, + { id: 7, name: "Room B" } + ], + presentation_action_types: [] + } + }, + mediaUploadListState: { + media_uploads: [] + }, + currentEventListState: { + events: [], + lastPage: 1, + currentPage: 1, + order: "id", + orderDir: 1, + totalEvents: 0, + term: "", + filters: {}, + extraColumns: [], + perPage: 10 + } + } + }); + + const roomAChip = screen.getByRole("button", { name: /Room A/ }); + await userEvent.click(within(roomAChip).getByTestId("CancelIcon")); + + expect(mockGridFilterState.setFilters).toHaveBeenCalledWith( + [{ ...roomB, id: "location_id-1" }], + "ANY" + ); + }); + test("switches to the newly saved view", async () => { saveFilterCriteria.mockReturnValue(() => Promise.resolve({ From 52b9d0f350a74b7a2c32d08270e5029bfd34f6c0 Mon Sep 17 00:00:00 2001 From: anna Date: Tue, 10 Nov 2026 11:44:12 -0600 Subject: [PATCH 5/7] fix: code cleanups --- .../components/toolbar/index.js | 28 ++++++++--- .../mui/bulk-edit-data-grid/index.js | 18 +++++-- .../events/summit-event-list-page/helpers.js | 25 ++++------ .../events/summit-event-list-page/index.js | 50 +++++++++---------- src/utils/constants.js | 1 + 5 files changed, 68 insertions(+), 54 deletions(-) diff --git a/src/components/mui/bulk-edit-data-grid/components/toolbar/index.js b/src/components/mui/bulk-edit-data-grid/components/toolbar/index.js index 00c7df9d6..defc090d9 100644 --- a/src/components/mui/bulk-edit-data-grid/components/toolbar/index.js +++ b/src/components/mui/bulk-edit-data-grid/components/toolbar/index.js @@ -11,7 +11,7 @@ * limitations under the License. * */ -import React, { cloneElement } from "react"; +import React from "react"; import PropTypes from "prop-types"; import T from "i18n-react/dist/i18n-react"; import { Box, Button, Tooltip } from "@mui/material"; @@ -20,6 +20,7 @@ import FileDownloadIcon from "@mui/icons-material/FileDownload"; import FileUploadIcon from "@mui/icons-material/FileUpload"; import { GridToolbarContainer } from "@mui/x-data-grid"; import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-input"; +import { GridFilter } from "openstack-uicore-foundation/lib/components/mui/grid-filter"; import MenuButton from "../../../menu-button"; import ColumnsButton from "../columns-button"; import { FILTER_ICON_SX, ICON_ONLY_BUTTON_SX } from "../../constants"; @@ -32,7 +33,7 @@ const Toolbar = ({ addLabel, importItems, importLabel, - filter, + filterProps, appliedFilters, onExport, searchProps, @@ -107,10 +108,13 @@ const Toolbar = ({ )} - {filter && - cloneElement(filter, { - iconSx: { ...FILTER_ICON_SX, ...filter.props.iconSx } - })} + {filterProps && ( + + )} {searchProps && ( @@ -134,7 +138,15 @@ Toolbar.propTypes = { PropTypes.shape({ label: PropTypes.string, onClick: PropTypes.func }) ), importLabel: PropTypes.string, - filter: PropTypes.node, + filterProps: PropTypes.shape({ + id: PropTypes.string.isRequired, + criterias: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + label: PropTypes.string.isRequired + }) + ).isRequired + }), appliedFilters: PropTypes.node, onExport: PropTypes.func, searchProps: PropTypes.shape({ @@ -156,7 +168,7 @@ Toolbar.defaultProps = { addLabel: "", importItems: [], importLabel: "", - filter: null, + filterProps: null, appliedFilters: null, onExport: null, searchProps: null diff --git a/src/components/mui/bulk-edit-data-grid/index.js b/src/components/mui/bulk-edit-data-grid/index.js index b88387b47..2e15421dd 100644 --- a/src/components/mui/bulk-edit-data-grid/index.js +++ b/src/components/mui/bulk-edit-data-grid/index.js @@ -98,7 +98,7 @@ const BulkEditDataGrid = ({ addLabel, importItems, importLabel, - filter, + filterProps, appliedFilters, onExport, searchProps, @@ -360,7 +360,7 @@ const BulkEditDataGrid = ({ addLabel, importItems, importLabel, - filter, + filterProps, appliedFilters, onExport, searchProps, @@ -432,8 +432,16 @@ BulkEditDataGrid.propTypes = { ), // tooltip / aria-label of the import icon importLabel: PropTypes.string, - // a uicore ; its iconSx is styled to match the toolbar - filter: PropTypes.node, + // id and criterias of the uicore GridFilter the toolbar renders + filterProps: PropTypes.shape({ + id: PropTypes.string.isRequired, + criterias: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + label: PropTypes.string.isRequired + }) + ).isRequired + }), // own row under the toolbar, e.g. chips for the applied filters appliedFilters: PropTypes.node, onExport: PropTypes.func, @@ -465,7 +473,7 @@ BulkEditDataGrid.defaultProps = { addLabel: "", importItems: [], importLabel: "", - filter: null, + filterProps: null, appliedFilters: null, onExport: null, searchProps: null, diff --git a/src/pages/events/summit-event-list-page/helpers.js b/src/pages/events/summit-event-list-page/helpers.js index f6a67929a..5363ec666 100644 --- a/src/pages/events/summit-event-list-page/helpers.js +++ b/src/pages/events/summit-event-list-page/helpers.js @@ -30,6 +30,7 @@ import { formatDate, formatDuration } from "../../../utils/methods"; import { DEFAULT_Z_INDEX, HIGH_Z_INDEX, + LONG_DATETIME_FORMAT, SECONDS_TO_MINUTES } from "../../../utils/constants"; import { buildNameIdDDL } from "../../../utils/events/summit-event-list-page.utils"; @@ -117,20 +118,20 @@ export const formatEventData = (e, summit) => { ? formatDate( e.published_date, summit.time_zone.name, - "MMMM Do YYYY, h:mm a" + LONG_DATETIME_FORMAT ) : "No", start_date_display: e.start_date - ? formatDate(e.start_date, summit.time_zone.name, "MMMM Do YYYY, h:mm a") + ? formatDate(e.start_date, summit.time_zone.name, LONG_DATETIME_FORMAT) : "TBD", end_date_display: e.end_date - ? formatDate(e.end_date, summit.time_zone.name, "MMMM Do YYYY, h:mm a") + ? formatDate(e.end_date, summit.time_zone.name, LONG_DATETIME_FORMAT) : "TBD", created_display: e.created - ? formatDate(e.created, summit.time_zone_id, "MMMM Do YYYY, h:mm a") + ? formatDate(e.created, summit.time_zone_id, LONG_DATETIME_FORMAT) : "TBD", modified: e.last_edited - ? formatDate(e.last_edited, summit.time_zone_id, "MMMM Do YYYY, h:mm a") + ? formatDate(e.last_edited, summit.time_zone_id, LONG_DATETIME_FORMAT) : "TBD" }; }; @@ -1116,21 +1117,17 @@ export const getFilterChipLabel = (filter, criterias) => { (op) => op.value === filter.operator ); const { type, props } = criteria.values; - const options = - typeof props?.options === "function" - ? props.options(filter.value) - : props?.options; + const options = props?.options; const toLabel = (v) => { // async inputs store option objects if (v != null && typeof v === "object") return v.label; - if (type === "datetime") - return formatDate(v, "LOC", "MMMM Do YYYY, h:mm a"); + if (type === "datetime") return formatDate(v, "LOC", LONG_DATETIME_FORMAT); return options?.find((o) => o.value === v)?.label ?? v; }; const values = Array.isArray(filter.value) ? filter.value : [filter.value]; + const operatorLabel = operator?.label ?? filter.operator; + const valueLabels = values.map(toLabel).join(", "); - return `${criteria.label}: ${operator?.label ?? filter.operator} ${values - .map(toLabel) - .join(", ")}`; + return `${criteria.label}: ${operatorLabel} ${valueLabels}`; }; diff --git a/src/pages/events/summit-event-list-page/index.js b/src/pages/events/summit-event-list-page/index.js index dc61eb956..d9ec3f34c 100644 --- a/src/pages/events/summit-event-list-page/index.js +++ b/src/pages/events/summit-event-list-page/index.js @@ -17,10 +17,7 @@ import T from "i18n-react/dist/i18n-react"; import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown"; import { Box, Chip } from "@mui/material"; import FilterAltIcon from "@mui/icons-material/FilterAlt"; -import { - GridFilter, - useGridFilter -} from "openstack-uicore-foundation/lib/components/mui/grid-filter"; +import { useGridFilter } from "openstack-uicore-foundation/lib/components/mui/grid-filter"; import BulkEditDataGrid from "../../../components/mui/bulk-edit-data-grid"; import { bulkUpdateEvents, @@ -317,6 +314,26 @@ const SummitEventListPage = ({ const tableData = events.map((e) => formatEventData(e, currentSummit)); const criterias = getCriterias(currentSummit, mediaUploadTypes); + const appliedFilters = valuesWithIds.length > 0 && ( + + {valuesWithIds.map((filter) => ( + } + label={getFilterChipLabel(filter, criterias)} + onDelete={() => handleRemoveFilter(filter.id)} + /> + ))} + + ); return (
@@ -332,29 +349,8 @@ const SummitEventListPage = ({ columns={tableColumns} columnVisibilityModel={columnVisibilityModel} onColumnVisibilityModelChange={handleColumnVisibilityModelChange} - filter={} - appliedFilters={ - filterValues.length > 0 && ( - - {valuesWithIds.map((filter) => ( - } - label={getFilterChipLabel(filter, criterias)} - onDelete={() => handleRemoveFilter(filter.id)} - /> - ))} - - ) - } + filterProps={{ id: FILTER_ID, criterias }} + appliedFilters={appliedFilters} views={ Date: Thu, 12 Nov 2026 17:30:24 -0600 Subject: [PATCH 6/7] fix: applied filters pulls from grid filter not a prop --- .../mui/__tests__/bulk-edit-data-grid.test.js | 50 ++++++++++++++ .../components/applied-filters/index.js | 66 ++++++++++++++++++ .../components/toolbar/index.js | 8 +-- .../mui/bulk-edit-data-grid/index.js | 5 -- .../__tests__/summit-event-list-page.test.js | 68 +------------------ .../events/summit-event-list-page/index.js | 40 +---------- 6 files changed, 125 insertions(+), 112 deletions(-) create mode 100644 src/components/mui/bulk-edit-data-grid/components/applied-filters/index.js diff --git a/src/components/mui/__tests__/bulk-edit-data-grid.test.js b/src/components/mui/__tests__/bulk-edit-data-grid.test.js index bdf0d187c..c4cf764d5 100644 --- a/src/components/mui/__tests__/bulk-edit-data-grid.test.js +++ b/src/components/mui/__tests__/bulk-edit-data-grid.test.js @@ -12,6 +12,21 @@ jest.mock("i18n-react/dist/i18n-react", () => ({ jest.mock("../showConfirmDialog", () => jest.fn()); +// tests seed the applied filters before rendering +const mockGridFilterState = { + valuesWithIds: [], + setFilters: jest.fn(), + joinOperator: undefined +}; + +jest.mock("openstack-uicore-foundation/lib/components/mui/grid-filter", () => ({ + ...jest.requireActual( + "openstack-uicore-foundation/lib/components/mui/grid-filter" + ), + GridFilter: () => null, + useGridFilter: () => mockGridFilterState +})); + const columns = [ { columnKey: "id", label: "Id", sortable: true }, { columnKey: "title", label: "Title", sortable: true, editableField: true } @@ -236,6 +251,41 @@ describe("BulkEditDataGrid", () => { expect(onSearch).toHaveBeenCalledWith("keynote"); }); + test("removing a filter chip keeps the other filters and the join operator", async () => { + const roomA = { criteria: "location_id", operator: "==", value: [2] }; + const roomB = { criteria: "location_id", operator: "==", value: [7] }; + mockGridFilterState.valuesWithIds = [ + { ...roomA, id: "location_id-0" }, + { ...roomB, id: "location_id-1" } + ]; + mockGridFilterState.joinOperator = "ANY"; + const criterias = [ + { + key: "location_id", + label: "Location", + operators: [{ value: "==", label: "is" }], + values: { + type: "select", + props: { + options: [ + { value: 2, label: "Room A" }, + { value: 7, label: "Room B" } + ] + } + } + } + ]; + renderGrid({ filterProps: { id: "grid_filter", criterias } }); + + const roomAChip = screen.getByRole("button", { name: /Room A/ }); + await userEvent.click(within(roomAChip).getByTestId("CancelIcon")); + + expect(mockGridFilterState.setFilters).toHaveBeenCalledWith( + [{ ...roomB, id: "location_id-1" }], + "ANY" + ); + }); + test("adds from the add button and lists every import option under the import icon", async () => { const onAdd = jest.fn(); const onMuxImport = jest.fn(); diff --git a/src/components/mui/bulk-edit-data-grid/components/applied-filters/index.js b/src/components/mui/bulk-edit-data-grid/components/applied-filters/index.js new file mode 100644 index 000000000..5045cbed1 --- /dev/null +++ b/src/components/mui/bulk-edit-data-grid/components/applied-filters/index.js @@ -0,0 +1,66 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +import React from "react"; +import PropTypes from "prop-types"; +import { Box, Chip } from "@mui/material"; +import FilterAltIcon from "@mui/icons-material/FilterAlt"; +import { useGridFilter } from "openstack-uicore-foundation/lib/components/mui/grid-filter"; +import { getFilterChipLabel } from "../../../../../pages/events/summit-event-list-page/helpers"; + +// own component so useGridFilter only runs when the grid has a filter +const AppliedFilters = ({ id, criterias }) => { + const { valuesWithIds, setFilters, joinOperator } = useGridFilter(id); + + if (valuesWithIds.length === 0) return null; + + const handleRemoveFilter = (filterId) => + setFilters( + valuesWithIds.filter((f) => f.id !== filterId), + joinOperator + ); + + return ( + + {valuesWithIds.map((filter) => ( + } + label={getFilterChipLabel(filter, criterias)} + onDelete={() => handleRemoveFilter(filter.id)} + /> + ))} + + ); +}; + +AppliedFilters.propTypes = { + id: PropTypes.string.isRequired, + criterias: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + label: PropTypes.string.isRequired + }) + ).isRequired +}; + +export default AppliedFilters; diff --git a/src/components/mui/bulk-edit-data-grid/components/toolbar/index.js b/src/components/mui/bulk-edit-data-grid/components/toolbar/index.js index defc090d9..7dc1d7d5b 100644 --- a/src/components/mui/bulk-edit-data-grid/components/toolbar/index.js +++ b/src/components/mui/bulk-edit-data-grid/components/toolbar/index.js @@ -23,6 +23,7 @@ import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-i import { GridFilter } from "openstack-uicore-foundation/lib/components/mui/grid-filter"; import MenuButton from "../../../menu-button"; import ColumnsButton from "../columns-button"; +import AppliedFilters from "../applied-filters"; import { FILTER_ICON_SX, ICON_ONLY_BUTTON_SX } from "../../constants"; const SEARCH_WIDTH = 330; @@ -34,7 +35,6 @@ const Toolbar = ({ importItems, importLabel, filterProps, - appliedFilters, onExport, searchProps, editEnabled, @@ -126,7 +126,9 @@ const Toolbar = ({ )} - {appliedFilters && {appliedFilters}} + {filterProps && ( + + )} ); @@ -147,7 +149,6 @@ Toolbar.propTypes = { }) ).isRequired }), - appliedFilters: PropTypes.node, onExport: PropTypes.func, searchProps: PropTypes.shape({ placeholder: PropTypes.string, @@ -169,7 +170,6 @@ Toolbar.defaultProps = { importItems: [], importLabel: "", filterProps: null, - appliedFilters: null, onExport: null, searchProps: null }; diff --git a/src/components/mui/bulk-edit-data-grid/index.js b/src/components/mui/bulk-edit-data-grid/index.js index 2e15421dd..e5d6c4ffa 100644 --- a/src/components/mui/bulk-edit-data-grid/index.js +++ b/src/components/mui/bulk-edit-data-grid/index.js @@ -99,7 +99,6 @@ const BulkEditDataGrid = ({ importItems, importLabel, filterProps, - appliedFilters, onExport, searchProps, noRowsLabel @@ -361,7 +360,6 @@ const BulkEditDataGrid = ({ importItems, importLabel, filterProps, - appliedFilters, onExport, searchProps, editEnabled, @@ -442,8 +440,6 @@ BulkEditDataGrid.propTypes = { }) ).isRequired }), - // own row under the toolbar, e.g. chips for the applied filters - appliedFilters: PropTypes.node, onExport: PropTypes.func, // same shape as GridToolbar's searchProps searchProps: PropTypes.shape({ @@ -474,7 +470,6 @@ BulkEditDataGrid.defaultProps = { importItems: [], importLabel: "", filterProps: null, - appliedFilters: null, onExport: null, searchProps: null, noRowsLabel: null diff --git a/src/pages/events/__tests__/summit-event-list-page.test.js b/src/pages/events/__tests__/summit-event-list-page.test.js index a5dc53e99..7218670b3 100644 --- a/src/pages/events/__tests__/summit-event-list-page.test.js +++ b/src/pages/events/__tests__/summit-event-list-page.test.js @@ -1,5 +1,5 @@ import React from "react"; -import { screen, act, within } from "@testing-library/react"; +import { screen, act } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; import flushPromises from "flush-promises"; @@ -32,14 +32,8 @@ jest.mock( () => function BulkEditDataGridMock(props) { mockEditableTableSpy(props); - // saved views and applied filter chips render inside the grid toolbar - const { views, appliedFilters } = props; - return ( - <> - {views} - {appliedFilters} - - ); + // the saved views dropdown is rendered inside the grid toolbar + return props.views; } ); @@ -112,7 +106,6 @@ const mockGridFilterState = { parsedFilter: [], resetFilters: jest.fn(), filterValues: [], - valuesWithIds: [], setFilters: jest.fn(), joinOperator: undefined }; @@ -140,7 +133,6 @@ describe("SummitEventListPage", () => { ); mockGridFilterState.parsedFilter = []; mockGridFilterState.filterValues = []; - mockGridFilterState.valuesWithIds = []; mockGridFilterState.joinOperator = undefined; saveFilterCriteria.mockReturnValue(() => Promise.resolve({ response: { id: 1, name: "view", criteria: [] } }) @@ -457,60 +449,6 @@ describe("SummitEventListPage", () => { ]); }); - test("removing a filter chip keeps the other filters and the join operator", async () => { - const roomA = { criteria: "location_id", operator: "==", value: [2] }; - const roomB = { criteria: "location_id", operator: "==", value: [7] }; - mockGridFilterState.filterValues = [roomA, roomB]; - mockGridFilterState.valuesWithIds = [ - { ...roomA, id: "location_id-0" }, - { ...roomB, id: "location_id-1" } - ]; - mockGridFilterState.joinOperator = "ANY"; - - renderWithRedux(, { - initialState: { - currentSummitState: { - currentSummit: { - id: 12, - time_zone: { name: "UTC" }, - time_zone_id: "UTC", - selection_plans: [], - tracks: [], - event_types: [], - locations: [ - { id: 2, name: "Room A" }, - { id: 7, name: "Room B" } - ], - presentation_action_types: [] - } - }, - mediaUploadListState: { - media_uploads: [] - }, - currentEventListState: { - events: [], - lastPage: 1, - currentPage: 1, - order: "id", - orderDir: 1, - totalEvents: 0, - term: "", - filters: {}, - extraColumns: [], - perPage: 10 - } - } - }); - - const roomAChip = screen.getByRole("button", { name: /Room A/ }); - await userEvent.click(within(roomAChip).getByTestId("CancelIcon")); - - expect(mockGridFilterState.setFilters).toHaveBeenCalledWith( - [{ ...roomB, id: "location_id-1" }], - "ANY" - ); - }); - test("switches to the newly saved view", async () => { saveFilterCriteria.mockReturnValue(() => Promise.resolve({ diff --git a/src/pages/events/summit-event-list-page/index.js b/src/pages/events/summit-event-list-page/index.js index d9ec3f34c..50bc227a2 100644 --- a/src/pages/events/summit-event-list-page/index.js +++ b/src/pages/events/summit-event-list-page/index.js @@ -15,8 +15,6 @@ import React, { useEffect, useState } from "react"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown"; -import { Box, Chip } from "@mui/material"; -import FilterAltIcon from "@mui/icons-material/FilterAlt"; import { useGridFilter } from "openstack-uicore-foundation/lib/components/mui/grid-filter"; import BulkEditDataGrid from "../../../components/mui/bulk-edit-data-grid"; import { @@ -47,7 +45,6 @@ import ImportMUXModal from "./components/ImportMUXModal"; import { formatEventData, getCriterias, - getFilterChipLabel, getOptionalColumns, packJoinOperatorIntoCriteria, toApiSortKey, @@ -87,14 +84,8 @@ const SummitEventListPage = ({ // remembered (selectedColumns), the default ones reset each visit const [gridColumnVisibility, setGridColumnVisibility] = useState({}); const [selectedFilterCriteria, setSelectedFilterCriteria] = useState(null); - const { - parsedFilter, - resetFilters, - filterValues, - valuesWithIds, - setFilters, - joinOperator - } = useGridFilter(FILTER_ID); + const { parsedFilter, resetFilters, filterValues, setFilters, joinOperator } = + useGridFilter(FILTER_ID); // eslint-disable-next-line no-underscore-dangle const _getEvents = (params = {}) => { @@ -232,12 +223,6 @@ const SummitEventListPage = ({ } }); - const handleRemoveFilter = (id) => - setFilters( - valuesWithIds.filter((f) => f.id !== id), - joinOperator - ); - const eventTypeOptions = buildNameIdDDL(currentSummit.event_types); const fixedColumns = [ @@ -314,26 +299,6 @@ const SummitEventListPage = ({ const tableData = events.map((e) => formatEventData(e, currentSummit)); const criterias = getCriterias(currentSummit, mediaUploadTypes); - const appliedFilters = valuesWithIds.length > 0 && ( - - {valuesWithIds.map((filter) => ( - } - label={getFilterChipLabel(filter, criterias)} - onDelete={() => handleRemoveFilter(filter.id)} - /> - ))} - - ); return (
@@ -350,7 +315,6 @@ const SummitEventListPage = ({ columnVisibilityModel={columnVisibilityModel} onColumnVisibilityModelChange={handleColumnVisibilityModelChange} filterProps={{ id: FILTER_ID, criterias }} - appliedFilters={appliedFilters} views={ Date: Fri, 13 Nov 2026 13:23:55 -0600 Subject: [PATCH 7/7] fix: strip filter id before resaving to redux --- src/components/mui/__tests__/bulk-edit-data-grid.test.js | 5 +---- .../bulk-edit-data-grid/components/applied-filters/index.js | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/mui/__tests__/bulk-edit-data-grid.test.js b/src/components/mui/__tests__/bulk-edit-data-grid.test.js index c4cf764d5..a488e7317 100644 --- a/src/components/mui/__tests__/bulk-edit-data-grid.test.js +++ b/src/components/mui/__tests__/bulk-edit-data-grid.test.js @@ -280,10 +280,7 @@ describe("BulkEditDataGrid", () => { const roomAChip = screen.getByRole("button", { name: /Room A/ }); await userEvent.click(within(roomAChip).getByTestId("CancelIcon")); - expect(mockGridFilterState.setFilters).toHaveBeenCalledWith( - [{ ...roomB, id: "location_id-1" }], - "ANY" - ); + expect(mockGridFilterState.setFilters).toHaveBeenCalledWith([roomB], "ANY"); }); test("adds from the add button and lists every import option under the import icon", async () => { diff --git a/src/components/mui/bulk-edit-data-grid/components/applied-filters/index.js b/src/components/mui/bulk-edit-data-grid/components/applied-filters/index.js index 5045cbed1..c86cd7c74 100644 --- a/src/components/mui/bulk-edit-data-grid/components/applied-filters/index.js +++ b/src/components/mui/bulk-edit-data-grid/components/applied-filters/index.js @@ -26,7 +26,9 @@ const AppliedFilters = ({ id, criterias }) => { const handleRemoveFilter = (filterId) => setFilters( - valuesWithIds.filter((f) => f.id !== filterId), + valuesWithIds + .filter((f) => f.id !== filterId) + .map(({ id: _id, ...filter }) => filter), joinOperator );