Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
131cb96
refactor: change activity list grid from coreui to mui data grid
ako3131 Oct 2, 2026
ac84f51
fix: add original pagination back to the top and bottom of grid
ako3131 Oct 2, 2026
bd54ead
fix: add sticky headers to the grid
ako3131 Oct 2, 2026
6eac9af
refactor: reorder grid header buttons
ako3131 Oct 2, 2026
094ed02
fix: move bottom pagination to under and outside of the grid to align…
ako3131 Oct 2, 2026
7568e28
refactor: add add activity and import to grid toolbar
ako3131 Oct 2, 2026
1739392
refactor: move saved views into the grid toolbar
ako3131 Oct 2, 2026
bc3928f
refactor: remove gap between activity list, pagination, and grid
ako3131 Oct 2, 2026
3282661
fix: increase size of import button
ako3131 Oct 2, 2026
73ee67e
feat: make selection status hideable
ako3131 Oct 2, 2026
99612b2
fix: columns modal should show under the button not the left side of …
ako3131 Oct 2, 2026
848a85e
fix: clicking the column button when the panel is open now closes it
ako3131 Oct 2, 2026
5d51c60
fix: increase the appearance of the add button
ako3131 Oct 2, 2026
80917c4
fix: actions column should not be able to be deselected
ako3131 Oct 5, 2026
423a2df
fix: remove nonexistant placeholder
ako3131 Oct 5, 2026
c227ff6
refactor: use ui core search not mui search in grid
ako3131 Oct 5, 2026
47155c1
fix: match user sorted arrow displays to match previous bulk edit table
ako3131 Oct 5, 2026
8b111ff
fix: use prexisting utilities
ako3131 Oct 5, 2026
87d1880
fix: fix unnecessary acceptance of string in delete dialog body
ako3131 Oct 5, 2026
b020b64
refactor: make add activity a text button
ako3131 Oct 5, 2026
c971dba
fix: saved views no longer briefly shows incorrect list
ako3131 Oct 6, 2026
c57bf1e
fix: bring back place holder text fallback
ako3131 Oct 6, 2026
68ca5c8
fix: test and comment clean up
ako3131 Oct 6, 2026
b34e1c0
fix: use the preexsting general name
ako3131 Oct 6, 2026
48d510d
fix: all columns are hideable, defaults reset and optional one are re…
ako3131 Oct 6, 2026
b3c2a7e
fix: keep grid column widths stable when resizing and truncate cell text
ako3131 Oct 9, 2026
7ef6ebd
fix: make added columns persist when navigating away or refresh
ako3131 Oct 19, 2026
d930c90
fix: speaker and selection plan persist during an activity type switc…
ako3131 Oct 19, 2026
f632809
fix: allow escape and tab keystrokes on saved views. replace deprecat…
ako3131 Oct 19, 2026
ae71970
fix: search box collapses when it is empty and no search term applied
ako3131 Oct 19, 2026
3dd0924
fix: add custom search placeholder and increase size of search bar to…
ako3131 Oct 21, 2026
c79f28b
fix: sort columns options by alphabetical order in column panel
ako3131 Oct 21, 2026
da73b09
fix: stop clearing list on a saved views dropdown open
ako3131 Oct 21, 2026
44d7e41
fix: remove update view functionality since it did not align with onS…
ako3131 Oct 22, 2026
59acb10
fix: remove unneccesary comment and make saved views pop up close on …
ako3131 Oct 22, 2026
c4b17a9
fix: use core ui dialog component for saved view pop up
ako3131 Oct 22, 2026
0de8a05
fix: move save view dialog into its own file
ako3131 Oct 22, 2026
5980a8c
fix: move tool bar buttons into their own files
ako3131 Oct 22, 2026
bf2c4dc
fix: make sure all columns are resizable when user is not editing
ako3131 Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@fontsource/roboto": "^5.1.1",
"@mui/icons-material": "^6.4.3",
"@mui/material": "^6.4.3",
"@mui/x-data-grid": "^7.29.13",
"@mui/x-date-pickers": "^7.26.0",
"@react-pdf/renderer": "^4.4.1",
"@sentry/react": "^8.32.0",
Expand Down
5 changes: 5 additions & 0 deletions src/actions/event-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const FLAG_CHANGED = "FLAG_CHANGED";
export const REQUEST_EVENT_COMMENTS = "REQUEST_EVENT_COMMENTS";
export const RECEIVE_EVENT_COMMENTS = "RECEIVE_EVENT_COMMENTS";
export const CHANGE_SEARCH_TERM = "CHANGE_SEARCH_TERM";
export const CHANGE_EXTRA_COLUMNS = "CHANGE_EXTRA_COLUMNS";
export const SUBMISSION_PERIOD_REOPENED = "SUBMISSION_PERIOD_REOPENED";
export const SUBMISSION_PERIOD_CLOSED = "SUBMISSION_PERIOD_CLOSED";
export const SUBMISSION_REOPEN_NOTIFIED = "SUBMISSION_REOPEN_NOTIFIED";
Expand Down Expand Up @@ -1529,3 +1530,7 @@ export const queryAllCompanies = debounce(async (input, callback) => {
export const changeEventListSearchTerm = (term) => (dispatch) => {
dispatch(createAction(CHANGE_SEARCH_TERM)({ term }));
};

export const changeEventListExtraColumns = (extraColumns) => (dispatch) => {
dispatch(createAction(CHANGE_EXTRA_COLUMNS)({ extraColumns }));
};
5 changes: 3 additions & 2 deletions src/actions/filter-criteria-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from "openstack-uicore-foundation/lib/utils/actions";
import URI from "urijs";
import Swal from "sweetalert2";
import debounce from "lodash/debounce"
import debounce from "lodash/debounce";

import { getAccessTokenSafely } from "../utils/methods";
import {
Expand Down Expand Up @@ -87,8 +87,9 @@ export const saveFilterCriteria = (filterCriteria) => async (dispatch) => {
`${window.PERSIST_FILTER_CRITERIA_API}/api/v1/filter-criterias`,
filterCriteria,
customErrorHandler
)(params)(dispatch).then(() => {
)(params)(dispatch).then((payload) => {
dispatch(stopLoading());
return payload;
});
};

Expand Down
237 changes: 237 additions & 0 deletions src/components/filters/saved-views/__tests__/saved-views.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import React from "react";
import { render, screen, waitFor, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";
import SavedViews from "..";
import showConfirmDialog from "../../../mui/showConfirmDialog";
import { queryFilterCriterias } from "../../../../actions/filter-criteria-actions";

jest.mock("i18n-react/dist/i18n-react", () => ({
__esModule: true,
default: { translate: (key) => key }
}));

jest.mock("../../../mui/showConfirmDialog", () => jest.fn());

jest.mock("../../../../actions/filter-criteria-actions", () => ({
queryFilterCriterias: jest.fn()
}));

const VIEWS = [
{ id: 1, name: "Open Activities", visibility: "Everyone", criteria: [] },
{ id: 2, name: "Recently Modified", visibility: "Me", criteria: [] }
];

const renderViews = (props = {}) =>
render(
<SavedViews
summitId={12}
context="Activities"
onChange={jest.fn()}
onSave={jest.fn(() => Promise.resolve())}
onDelete={jest.fn(() => Promise.resolve())}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
);

const openMenu = (label = "saved_views.title") =>
userEvent.click(screen.getByRole("button", { name: label }));

describe("SavedViews", () => {
beforeEach(() => {
jest.clearAllMocks();
queryFilterCriterias.mockImplementation((summitId, context, input, cb) =>
cb(VIEWS.filter((v) => v.name.includes(input)))
);
});

test("lists the show's views for this context and applies the one picked", async () => {
const onChange = jest.fn();
renderViews({ onChange });

await openMenu();

expect(queryFilterCriterias).toHaveBeenCalledWith(
12,
"Activities",
"",
expect.any(Function)
);
await userEvent.click(
screen.getByRole("menuitem", { name: /Recently Modified/ })
);

expect(onChange).toHaveBeenCalledWith(VIEWS[1]);
});

test("says there are no saved views only once they've loaded empty", async () => {
let resolveViews;
queryFilterCriterias.mockImplementation((summitId, context, input, cb) => {
resolveViews = cb;
});
renderViews();

await openMenu();
expect(
screen.queryByRole("menuitem", { name: "saved_views.no_views" })
).not.toBeInTheDocument();

act(() => resolveViews([]));
expect(
screen.getByRole("menuitem", { name: "saved_views.no_views" })
).toBeInTheDocument();
});

test("searches views by name", async () => {
renderViews();

await openMenu();
await userEvent.type(
screen.getByRole("textbox", { name: "general.search" }),
"Open"
);

expect(queryFilterCriterias).toHaveBeenLastCalledWith(
12,
"Activities",
"Open",
expect.any(Function)
);
});

test("deletes a view after confirming, without applying it", async () => {
const onChange = jest.fn();
const onDelete = jest.fn(() => Promise.resolve());
showConfirmDialog.mockResolvedValueOnce(true);
renderViews({ onChange, onDelete });

await openMenu();
await userEvent.click(
screen.getByRole("button", { name: "general.delete Open Activities" })
);

await waitFor(() => expect(onDelete).toHaveBeenCalledWith(1));
await waitFor(() =>
expect(
screen.queryByRole("menuitem", { name: /Open Activities/ })
).not.toBeInTheDocument()
);
expect(onChange).not.toHaveBeenCalled();
});

test("keeps the view when the delete is cancelled", async () => {
const onDelete = jest.fn();
showConfirmDialog.mockResolvedValueOnce(false);
renderViews({ onDelete });

await openMenu();
await userEvent.click(
screen.getByRole("button", { name: "general.delete Open Activities" })
);

await waitFor(() => expect(showConfirmDialog).toHaveBeenCalled());
expect(onDelete).not.toHaveBeenCalled();
});

test("offers clearing only while a view is active", async () => {
const onChange = jest.fn();
const { rerender } = renderViews({ onChange });

await openMenu();
expect(
screen.queryByRole("menuitem", { name: "saved_views.clear" })
).not.toBeInTheDocument();
await userEvent.keyboard("{Escape}");

rerender(
<SavedViews
summitId={12}
context="Activities"
selectedView={VIEWS[0]}
onChange={onChange}
onSave={jest.fn()}
onDelete={jest.fn()}
/>
);
await openMenu("Open Activities");
await userEvent.click(
screen.getByRole("menuitem", { name: "saved_views.clear" })
);

expect(onChange).toHaveBeenCalledWith(null);
});

test("requires a name and visibility before saving", async () => {
const onSave = jest.fn(() => Promise.resolve());
renderViews({ onSave });

await openMenu();
await userEvent.click(
screen.getByRole("menuitem", { name: "saved_views.save_current" })
);
await userEvent.click(screen.getByRole("button", { name: "general.save" }));

expect(await screen.findAllByText("validation.required")).toHaveLength(2);
expect(onSave).not.toHaveBeenCalled();
});

test("starts with an empty name and closes once the save resolves", async () => {
const onSave = jest.fn(() => Promise.resolve());
renderViews({ onSave, selectedView: VIEWS[0] });

await openMenu("Open Activities");
await userEvent.click(
screen.getByRole("menuitem", { name: "saved_views.save_current" })
);

const name = screen.getByRole("textbox", { name: "general.name" });
expect(name).toHaveValue("");
await userEvent.type(name, "Open Keynotes");
await userEvent.click(screen.getByRole("button", { name: "general.save" }));

expect(onSave).toHaveBeenCalledWith({
name: "Open Keynotes",
visibility: "Everyone"
});
await waitFor(() =>
expect(screen.queryByRole("dialog")).not.toBeInTheDocument()
);
});

test("closes the dialog when the save fails so the error isn't covered", async () => {
let rejectSave;
const onSave = jest.fn(
() =>
new Promise((resolve, reject) => {
rejectSave = reject;
})
);
renderViews({ onSave });

await openMenu();
await userEvent.click(
screen.getByRole("menuitem", { name: "saved_views.save_current" })
);
await userEvent.type(
screen.getByRole("textbox", { name: "general.name" }),
"Mine"
);
await userEvent.click(
screen.getByRole("radio", { name: "save_filter_criteria.me" })
);
await userEvent.click(screen.getByRole("button", { name: "general.save" }));

await waitFor(() =>
expect(
screen.getByRole("button", { name: "general.save" })
).toBeDisabled()
);
expect(screen.getByRole("dialog")).toBeInTheDocument();
await act(async () => rejectSave(new Error("nope")));

await waitFor(() =>
expect(screen.queryByRole("dialog")).not.toBeInTheDocument()
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* 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 T from "i18n-react/dist/i18n-react";
import { FormikProvider, useFormik } from "formik";
import * as yup from "yup";
import { Box } from "@mui/material";
import MuiFormikTextField from "openstack-uicore-foundation/lib/components/mui/formik-inputs/textfield";
import MuiFormikRadioGroup from "openstack-uicore-foundation/lib/components/mui/formik-inputs/radio-group";
import CustomDialog from "openstack-uicore-foundation/lib/components/mui/custom-dialog";
import { requiredStringValidation } from "../../../../../utils/yup";
import {
VISIBILITY_OPTION_EVERYONE,
VISIBILITY_OPTION_ME
} from "../../../../../utils/filter-criteria-constants";

const SaveViewDialog = ({ selectedView, onSave, onClose }) => {
const formik = useFormik({
initialValues: {
name: "",
visibility: selectedView?.visibility ?? ""
},
validationSchema: yup.object({
name: requiredStringValidation().trim(),
visibility: requiredStringValidation()
}),
onSubmit: (values) =>
onSave(values)
.finally(() => onClose())
.catch(() => {})
});

const handleClose = () => {
if (formik.isSubmitting) return;
onClose();
};

return (
<CustomDialog
title={T.translate("saved_views.save_current")}
open
onClose={handleClose}
maxWidth="xs"
primaryAction={{
label: T.translate("general.save"),
onClick: formik.submitForm
}}
secondaryAction={{
label: T.translate("general.cancel"),
onClick: handleClose
}}
>
<FormikProvider value={formik}>
{/* keeps Enter in the name field submitting */}
<Box
component="form"
onSubmit={formik.handleSubmit}
noValidate
autoComplete="off"
>
<MuiFormikTextField
name="name"
label={T.translate("general.name")}
fullWidth
autoFocus
/>
<MuiFormikRadioGroup
name="visibility"
label={T.translate("save_filter_criteria.visible_to")}
row
options={[
{
value: VISIBILITY_OPTION_ME,
label: T.translate("save_filter_criteria.me")
},
{
value: VISIBILITY_OPTION_EVERYONE,
label: T.translate("save_filter_criteria.everyone")
}
]}
/>
</Box>
</FormikProvider>
</CustomDialog>
);
};

SaveViewDialog.propTypes = {
selectedView: PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
visibility: PropTypes.string
}),
onSave: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};

SaveViewDialog.defaultProps = {
selectedView: null
};

export default SaveViewDialog;
Loading
Loading