diff --git a/package.json b/package.json
index 0e869b5fc..4aedaa4ed 100644
--- a/package.json
+++ b/package.json
@@ -92,7 +92,7 @@
"moment": "^2.29.1",
"moment-duration-format": "^2.3.2",
"moment-timezone": "^0.5.33",
- "openstack-uicore-foundation": "5.0.36-beta.1",
+ "openstack-uicore-foundation": "5.0.36-beta.2",
"p-limit": "^6.1.0",
"path-browserify": "^1.0.1",
"postcss-loader": "^6.2.1",
diff --git a/src/actions/event-actions.js b/src/actions/event-actions.js
index 60a669f89..330e5e809 100644
--- a/src/actions/event-actions.js
+++ b/src/actions/event-actions.js
@@ -51,7 +51,6 @@ import {
FIVE_PER_PAGE,
HOUR_AND_HALF
} from "../utils/constants";
-import { getIdValue } from "../utils/summitUtils";
URI.escapeQuerySpace = false;
@@ -195,12 +194,12 @@ export const normalizeBulkEvents = (entity) => {
const normalizedEvent = {
id: e.id,
title: e.title,
- selection_plan_id: getIdValue(e.selection_plan) || e.selection_plan_id,
+ selection_plan_id: e.selection_plan?.id || e.selection_plan_id,
location_id: e.location?.id || e.location_id,
start_date: e.start_date,
end_date: e.end_date,
- type_id: getIdValue(e.type) || e.type_id,
- track_id: getIdValue(e.track) || e.track_id,
+ type_id: e.type?.id || e.type_id,
+ track_id: e.track?.id || e.track_id,
duration: e.duration,
streaming_url: e.streaming_url,
streaming_type: e.streaming_type,
@@ -299,66 +298,65 @@ export const getEvents =
});
};
-export const bulkUpdateEvents =
- (summitId, events) => async (dispatch, getState) => {
- const { currentSummitState } = getState();
- const accessToken = await getAccessTokenSafely();
- const { currentSummit } = currentSummitState;
- dispatch(startLoading());
+export const bulkUpdateEvents = (events) => async (dispatch, getState) => {
+ const { currentSummitState } = getState();
+ const accessToken = await getAccessTokenSafely();
+ const { currentSummit } = currentSummitState;
+ dispatch(startLoading());
- const normalizedEvents = normalizeBulkEvents(
- events.map((event) =>
- normalizeEvent(
- event,
- currentSummit.event_types.find((et) => et.id === event.type_id)
- )
+ const normalizedEvents = normalizeBulkEvents(
+ events.map((event) =>
+ normalizeEvent(
+ event,
+ currentSummit.event_types.find((et) => et.id === event.type_id)
)
- );
+ )
+ );
- return putRequest(
- null,
- createAction(UPDATED_REMOTE_EVENTS)({}),
- `${window.API_BASE_URL}/api/v1/summits/${summitId}/events/?access_token=${accessToken}`,
- {
- events: normalizedEvents
- },
- authErrorHandler
- )({})(dispatch)
- .then(() => {
- dispatch(stopLoading());
- dispatch(
- showSuccessMessage(
- T.translate("bulk_actions_page.messages.update_success"),
- () => history.push(`/app/summits/${currentSummit.id}/events/`)
- )
- );
- const {
- currentEventListState: {
- term,
- currentPage,
- perPage,
- order,
- orderDir,
- filters,
- extraColumns
- }
- } = getState();
- dispatch(
- getEvents(
- term,
- currentPage,
- perPage,
- order,
- orderDir,
- filters,
- extraColumns
- )
- );
- })
- .catch(() => {
- console.log("ERROR");
- });
- };
+ return putRequest(
+ null,
+ createAction(UPDATED_REMOTE_EVENTS)({}),
+ `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/events/?access_token=${accessToken}`,
+ {
+ events: normalizedEvents
+ },
+ authErrorHandler
+ )({})(dispatch)
+ .then(() => {
+ dispatch(stopLoading());
+ dispatch(
+ showSuccessMessage(
+ T.translate("bulk_actions_page.messages.update_success"),
+ () => history.push(`/app/summits/${currentSummit.id}/events/`)
+ )
+ );
+ const {
+ currentEventListState: {
+ term,
+ currentPage,
+ perPage,
+ order,
+ orderDir,
+ filters,
+ extraColumns
+ }
+ } = getState();
+ dispatch(
+ getEvents(
+ term,
+ currentPage,
+ perPage,
+ order,
+ orderDir,
+ filters,
+ extraColumns
+ )
+ );
+ })
+ .catch(() => {
+ console.log("ERROR");
+ });
+};
export const getActionTypes =
(selectionPlanId) => async (dispatch, getState) => {
diff --git a/src/components/tables/editable-table/EditableTable.js b/src/components/tables/editable-table/EditableTable.js
deleted file mode 100644
index a8f959d07..000000000
--- a/src/components/tables/editable-table/EditableTable.js
+++ /dev/null
@@ -1,269 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { Tooltip } from "react-tooltip";
-import T from "i18n-react/dist/i18n-react";
-import EditableTableHeading from "./EditableTableHeading";
-import EditableTableRow from "./EditableTableRow";
-import {
- INDEX_NOT_FOUND,
- SORT_ASCENDING,
- SORT_DESCENDING
-} from "../../../utils/constants";
-
-import styles from "./index.module.less";
-
-const defaults = {
- sortFunc: (a, b) => {
- if (a < b) {
- return SORT_DESCENDING;
- }
- if (a > b) {
- return SORT_ASCENDING;
- }
- return 0;
- },
- sortable: false,
- sortCol: 0,
- sortDir: 1,
- colWidth: ""
-};
-
-function EditableTable(props) {
- const {
- options,
- columns,
- currentSummit,
- page,
- data,
- handleSort,
- updateData,
- handleDeleteRow,
- formattingFunction,
- afterUpdate = []
- } = props;
- let tableClass = options.hasOwnProperty("className") ? options.className : "";
- const [editButton, setEditButton] = useState(false);
- const [editEnabled, setEditEnabled] = useState(false);
- const [selected, setSelected] = useState([]);
- const [selectAll, setSelectAll] = useState(false);
- tableClass += options.actions?.edit ? " table-hover" : "";
-
- const getSortDir = (columnKey, columnIndex, sortCol, sortDir) => {
- if (columnKey && columnKey === sortCol) {
- return sortDir;
- }
- if (sortCol === columnIndex) {
- return sortDir;
- }
- return null;
- };
-
- const resetState = () => {
- setSelectAll(false);
- setEditButton(false);
- setEditEnabled(false);
- setSelected([]);
- };
-
- // reseting states on data changes/pagination
- useEffect(() => {
- resetState();
- }, [page]);
-
- useEffect(() => {
- if (selected.length > 0) {
- setEditButton(true);
- } else {
- setEditButton(false);
- setEditEnabled(false);
- }
- }, [selected]);
-
- useEffect(() => {
- if (selectAll) {
- setSelected(data);
- setSelectAll(true);
- } else {
- setSelectAll(false);
- setSelected([]);
- }
- }, [selectAll]);
-
- const updateSelected = (row, checked) => {
- const selectedRow = row;
- const rowIndex = selected.findIndex((s) => s.id === selectedRow.id);
- const exists = rowIndex !== INDEX_NOT_FOUND;
-
- if (checked) {
- if (exists) {
- // if already on selected list, replace with new data
- const updatedSelected = { ...selectedRow };
- const newSelected = selected.slice();
- newSelected[rowIndex] = updatedSelected;
- setSelected(newSelected);
- } else {
- // append to list
- setSelected((currSelected) => [...currSelected, selectedRow]);
- }
- } else {
- setSelected(selected.filter((se) => se.id !== selectedRow.id));
- }
- };
-
- const handledAfterUpdateData = () => {
- const actionsAfterUpdate = [];
- if (afterUpdate.length > 0) {
- afterUpdate.forEach(({ action, propertyName }) => {
- selected.forEach((row) => {
- if (Array.isArray(row[propertyName])) {
- row[propertyName].forEach((e) => {
- actionsAfterUpdate.push(action(e));
- });
- } else {
- actionsAfterUpdate.push(action(row[propertyName]));
- }
- });
- });
- }
- return Promise.all(actionsAfterUpdate);
- };
-
- const onUpdateEvents = (evt) => {
- evt.stopPropagation();
- evt.preventDefault();
- updateData(currentSummit.id, selected)
- .then(() => handledAfterUpdateData())
- .then(() => resetState())
- .catch((error) => {
- console.error("Error updating events:", error);
- });
- };
-
- return (
-
-
-
-
- {editEnabled ? (
- <>
-
-
- >
- ) : (
-
- )}
-
-
-
-
-
- );
-}
-
-export default EditableTable;
diff --git a/src/components/tables/editable-table/EditableTableHeading.js b/src/components/tables/editable-table/EditableTableHeading.js
deleted file mode 100644
index 17578effb..000000000
--- a/src/components/tables/editable-table/EditableTableHeading.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import React from "react";
-import PropTypes from "prop-types";
-import { SORT_ASCENDING, SORT_DESCENDING } from "../../../utils/constants";
-
-function EditableTableHeading(props) {
- const {
- editEnabled,
- sortable,
- sortDir,
- onSort,
- columnIndex,
- columnKey,
- sortFunc,
- width,
- children
- } = props;
- const getSortClass = () => {
- // disable sorting if on edit mode
- if (!sortable || editEnabled) return null;
-
- switch (sortDir) {
- case SORT_ASCENDING:
- return "sorting_asc";
- case SORT_DESCENDING:
- return "sorting_desc";
- default:
- return sortable ? "sorting" : null;
- }
- };
-
- const handleSort = (e) => {
- e.preventDefault();
-
- if (!onSort || !sortable) return;
-
- onSort(
- columnIndex,
- columnKey,
- sortDir ? sortDir * SORT_DESCENDING : SORT_ASCENDING,
- sortFunc
- );
- };
-
- return (
-
- {children}
- |
- );
-}
-
-EditableTableHeading.propTypes = {
- onSort: PropTypes.func,
- sortDir: PropTypes.number,
- columnIndex: PropTypes.number,
- columnKey: PropTypes.any,
- sortable: PropTypes.bool,
- sortFunc: PropTypes.func
-};
-
-export default EditableTableHeading;
diff --git a/src/components/tables/editable-table/EditableTableRow.js b/src/components/tables/editable-table/EditableTableRow.js
deleted file mode 100644
index bcd49f6f7..000000000
--- a/src/components/tables/editable-table/EditableTableRow.js
+++ /dev/null
@@ -1,190 +0,0 @@
-import React, { useEffect, useState } from "react";
-import TextArea from "openstack-uicore-foundation/lib/components/inputs/textarea-input";
-import T from "i18n-react/dist/i18n-react";
-import history from "../../../history";
-
-import styles from "./index.module.less";
-
-function EditableTableRow(props) {
- const {
- row,
- columns,
- editEnabled,
- selected,
- updateSelected,
- deleteRow,
- selectAll,
- currentSummit,
- actions,
- formattingFunction
- } = props;
- const [checked, setChecked] = useState(false);
- const [editData, setEditData] = useState(row);
-
- const formattedData = formattingFunction(row, currentSummit);
-
- useEffect(() => {
- setEditData(row);
- }, [row]);
- useEffect(() => {
- updateSelected(editData, checked);
- }, [checked, row]);
- useEffect(() => {
- setChecked(selectAll);
- }, [selectAll]);
- useEffect(() => {
- if (selected.length === 0) {
- setChecked(false);
- }
- }, [selected]);
- useEffect(() => {
- updateSelected(editData, checked);
- }, [editData]);
- useEffect(() => {
- if (!editEnabled) {
- setEditData(row);
- }
- }, [editEnabled]);
-
- const onRowChange = (ev) => {
- const { value, id } = ev.target;
- if (id.includes("___")) {
- const parts = id.split("___"); // ['array property', '', 'element property']
- const arrayProp = parts[0];
- const elementId = parseInt(parts[1], 10);
- const prop = parts[2];
-
- const arrayToChange = editData[arrayProp].map((elem) => {
- if (elem.id === elementId) {
- return { ...elem, [prop]: value };
- }
- return elem;
- });
-
- const newEventData = { ...editData, [arrayProp]: arrayToChange };
- setEditData(newEventData);
- } else {
- const newEventData = { ...editData, [id]: value };
- setEditData(newEventData);
- }
- };
-
- const onRemoveOption = (rowId, id) => {
- const currentRow = selected.find((r) => r.id === row.id);
- const newOptions = currentRow[id].filter((s) => s.id !== rowId);
- const newEventData = { ...editData, [id]: newOptions };
- setEditData(newEventData);
- };
-
- return (
- <>
-
- setChecked(!checked)}
- checked={checked}
- />
- |
- {row.id} |
- {selected.find((s) => s.id === row.id) && editEnabled && checked ? (
- <>
- {columns.map((col) => {
- if (col.columnKey === "id") {
- return null;
- }
- if (col.editableField === true) {
- // Default field as text
- return (
-
-
- |
- );
- }
- if (col.editableField) {
- return (
-
- {col.editableField({
- value:
- editData[col.columnKey]?.id ||
- editData[col.columnKey]?.value ||
- editData[col.columnKey],
- onChange: onRowChange,
- row: editData,
- rowData: editData[col.columnKey],
- onRemoveOption
- })}
- |
- );
- }
- return (
-
- {col.render
- ? col.render(row[col.columnKey], row)
- : formattedData[col.columnKey]}
- |
- );
- })}
- >
- ) : (
- columns.map(
- (col) =>
- col.columnKey !== "id" && (
-
- {col.render
- ? col.render(row[col.columnKey], row)
- : formattedData[col.columnKey]}
- |
- )
- )
- )}
- {(actions.edit || actions.delete) && (
-
-
- {actions.edit && (
-
- )}
- {actions.delete && (
-
- )}
-
- |
- )}
- >
- );
-}
-
-export default EditableTableRow;
diff --git a/src/components/tables/editable-table/__tests__/EditableTable.test.js b/src/components/tables/editable-table/__tests__/EditableTable.test.js
deleted file mode 100644
index 189b36a09..000000000
--- a/src/components/tables/editable-table/__tests__/EditableTable.test.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import React from "react";
-import userEvent from "@testing-library/user-event";
-import { act, render, screen, waitFor } from "@testing-library/react";
-import EditableTable from "../EditableTable";
-
-describe("EditableTable", () => {
- const baseProps = {
- options: {
- className: "test-table",
- actions: {}
- },
- columns: [
- { columnKey: "id", value: "id", sortable: true },
- { columnKey: "title", value: "title", sortable: true }
- ],
- currentSummit: { id: 99 },
- page: 1,
- handleSort: jest.fn(),
- handleDeleteRow: jest.fn(),
- formattingFunction: (row) => row,
- data: [
- {
- id: 1,
- title: "Event 1",
- media_uploads: [{ id: 11, event_id: 1 }]
- },
- {
- id: 2,
- title: "Event 2",
- media_uploads: [{ id: 22, event_id: 2 }]
- }
- ]
- };
-
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- test("applies bulk updates without executing afterUpdate by default", async () => {
- const user = userEvent.setup();
- const updateData = jest.fn(() => Promise.resolve());
-
- render();
-
- const checkboxes = screen.getAllByRole("checkbox");
-
- await user.click(checkboxes[1]);
- await user.click(screen.getByText("event_list.edit_selected"));
- await act(async () => {
- await user.click(screen.getByText("bulk_actions_page.btn_apply_changes"));
- });
-
- await waitFor(() => {
- expect(updateData).toHaveBeenCalledTimes(1);
- expect(updateData).toHaveBeenCalledWith(
- 99,
- expect.arrayContaining([expect.objectContaining({ id: 1 })])
- );
- });
- });
-
- test("executes afterUpdate actions only when explicitly configured", async () => {
- const user = userEvent.setup();
- const updateData = jest.fn(() => Promise.resolve());
- const afterUpdateAction = jest.fn(() => Promise.resolve());
-
- render(
-
- );
-
- const checkboxes = screen.getAllByRole("checkbox");
-
- await user.click(checkboxes[1]);
- await user.click(screen.getByText("event_list.edit_selected"));
- await act(async () => {
- await user.click(screen.getByText("bulk_actions_page.btn_apply_changes"));
- });
-
- await waitFor(() => {
- expect(updateData).toHaveBeenCalledTimes(1);
- expect(afterUpdateAction).toHaveBeenCalledTimes(1);
- expect(afterUpdateAction).toHaveBeenCalledWith(
- expect.objectContaining({ id: 11, event_id: 1 })
- );
- });
- });
-});
diff --git a/src/components/tables/editable-table/index.module.less b/src/components/tables/editable-table/index.module.less
deleted file mode 100644
index 0af7345a4..000000000
--- a/src/components/tables/editable-table/index.module.less
+++ /dev/null
@@ -1,70 +0,0 @@
-.editableTableWrapper {
- width: 100%;
-
- tbody > tr:nth-of-type(even) {
- background-color: #ffffff;
- }
-
- .tableWrapper {
- width: 100%;
- overflow-x: auto;
- position: relative;
-
- td {
- max-width: 150px;
- text-overflow: ellipsis;
- vertical-align: middle;
-
- &.checkColumn {
- display: table-cell;
- text-align: center;
- position: sticky;
- z-index: 5;
- left: 0;
- background-color: inherit;
- min-width: auto;
- }
- &.idColumn {
- min-width: auto;
- }
-
- &:not([class]) {
- min-width: 150px;
- align-self: flex-start;
- }
- }
-
- .bulkEditRow {
- display: flex;
- flex-wrap: nowrap;
- gap: 0 30px;
- align-items: baseline;
- padding-bottom: 10px;
- }
-
- .bulkEditCol {
- min-width: 250px;
- align-self: flex-start;
- }
-
- .actionColumn {
- display: table-cell;
- padding: 0 20px;
- text-align: right;
- position: sticky;
- z-index: 5;
- right: 0;
- background-color: inherit;
-
- .editButtonWrapper {
- display: flex;
- }
-
- i {
- padding: 0 10px;
- display: inline;
- cursor: pointer;
- }
- }
- }
-}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 1092c1a45..8947ceee4 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -1155,18 +1155,18 @@
"add_event": "Add Activity",
"title": "Title",
"type": "Activity Type",
- "event_type_capacity": "Select..",
- "selection_plan": "Select Selection Plan",
- "location": "Select Location",
- "level": "Select Level",
+ "event_type_capacity": "Activity Type Capacity",
+ "selection_plan": "Selection Plan",
+ "location": "Location",
+ "level": "Activity Level",
"tags": "Tags",
"progress_flags": "Progress Flags",
"streaming_url": "Streaming URL",
"meeting_url": "Meeting URL",
"etherpad_link": "Etherpad URL",
"streaming_type": "Streaming Type",
- "selection_status": "Select Status",
- "submission_status": "Select Status",
+ "selection_status": "Selection Status",
+ "submission_status": "Submission Status",
"published": "Published",
"speakers": "Speakers",
"duration": "Duration",
@@ -1175,12 +1175,14 @@
"submitter_company": "Submitter Company",
"sponsor": "Sponsors",
"all_companies": "All Companies",
- "track": "Select Activity Category",
+ "track": "Activity Category",
"start_date": "Start Date",
"end_date": "End Date",
"media_uploads_display": "Display on Site",
"allow_feedback": "Allow feedback ?",
"to_record": "To record ?",
+ "created_by": "Submitter",
+ "review_status": "Review Status",
"delete_event_warning": "Are you sure you want to delete activity",
"import": "Import",
"import_events": "Import Activity",
@@ -1193,14 +1195,12 @@
"mux_token_secret_info": "MUX Token Secret",
"mux_email_to": "Email to",
"mux_email_to_info": "Recipient to send process excerpt.",
- "created_by": "Submitter",
"mux_import_done": "MUX MP4 Import Process Requested Successfuly.",
"allows_attendee_vote_filter": "Allows attendee vote",
"allows_location_filter": "Allows location",
"allows_publishing_dates_filter": "Allows publishing dates",
"apply_filters": "Apply Filters",
"select_fields": "Show Columns",
- "review_status": "Review Status",
"created": "Created",
"modified": "Modified",
"submission_source": "Submission Source",
diff --git a/src/pages/events/__tests__/format-event-data.test.js b/src/pages/events/__tests__/format-event-data.test.js
new file mode 100644
index 000000000..077ecd13c
--- /dev/null
+++ b/src/pages/events/__tests__/format-event-data.test.js
@@ -0,0 +1,113 @@
+import { formatEventData } from "../summit-event-list-page/helpers";
+
+describe("summit-event-list-page.formatEventData", () => {
+ const summit = {
+ time_zone: { name: "UTC" },
+ time_zone_id: "UTC"
+ };
+
+ test("returns speakers_count based on speakers length when type.use_speakers is true", () => {
+ const event = {
+ id: 10,
+ summit_id: 1,
+ title: "Test Event",
+ is_published: false,
+ type: {
+ use_speakers: true,
+ allows_location: false,
+ allows_attendee_vote: false,
+ allows_publishing_dates: false
+ },
+ speakers: [
+ { first_name: "Jane", last_name: "Doe", company: "ACME" },
+ { first_name: "John", last_name: "Smith", company: "Globex" }
+ ],
+ tags: [],
+ sponsors: []
+ };
+
+ const result = formatEventData(event, summit);
+
+ expect(result.speakers_count).toBe(2);
+ });
+
+ test("returns 0 when type.use_speakers is true and speakers list is empty", () => {
+ const event = {
+ id: 12,
+ summit_id: 1,
+ title: "No Speakers Yet",
+ is_published: false,
+ type: {
+ use_speakers: true,
+ allows_location: false,
+ allows_attendee_vote: false,
+ allows_publishing_dates: false
+ },
+ speakers: [],
+ tags: [],
+ sponsors: []
+ };
+
+ const result = formatEventData(event, summit);
+
+ expect(result.speakers_count).toBe(0);
+ });
+
+ test("returns N/A and does not throw when event type is missing", () => {
+ const event = {
+ id: 11,
+ summit_id: 1,
+ title: "Type-less Event",
+ is_published: false,
+ speakers: [],
+ tags: [],
+ sponsors: []
+ };
+
+ let result;
+ expect(() => {
+ result = formatEventData(event, summit);
+ }).not.toThrow();
+ expect(result.speakers_count).toBe("N/A");
+ });
+
+ test("keeps real event fields raw so the row can be sent back to bulkUpdateEvents unchanged", () => {
+ const speakers = [{ id: 1, first_name: "Jane", last_name: "Doe" }];
+ const track = { id: 5, name: "Track A" };
+ const tags = [{ tag: "ai" }, { tag: "cloud" }];
+ const event = {
+ id: 13,
+ summit_id: 1,
+ title: "Raw Fields Event",
+ is_published: false,
+ type: { use_speakers: true },
+ speakers,
+ track,
+ tags,
+ sponsors: [],
+ location: { id: 7, name: "Hall A" },
+ start_date: 1700000000,
+ end_date: 1700003600,
+ duration: 3600,
+ level: "beginner",
+ selection_status: "unaccepted"
+ };
+
+ const result = formatEventData(event, summit);
+
+ expect(result.speakers).toBe(speakers);
+ expect(result.track).toBe(track);
+ expect(result.tags).toBe(tags);
+ expect(result.location).toEqual({ id: 7, name: "Hall A" });
+ expect(result.start_date).toBe(1700000000);
+ expect(result.end_date).toBe(1700003600);
+ expect(result.duration).toBe(3600);
+ expect(result.level).toBe("beginner");
+ expect(result.selection_status).toBe("unaccepted");
+
+ // display-only companions added alongside the raw fields
+ expect(result.speaker_names).toBe("Jane Doe");
+ expect(result.track_name).toBe("Track A");
+ expect(typeof result.start_date_display).toBe("string");
+ });
+});
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 e932bbb16..038902e16 100644
--- a/src/pages/events/__tests__/summit-event-list-page.test.js
+++ b/src/pages/events/__tests__/summit-event-list-page.test.js
@@ -5,9 +5,9 @@ import { renderWithRedux } from "../../../utils/test-utils";
const mockEditableTableSpy = jest.fn(() => null);
jest.mock(
- "../../../components/tables/editable-table/EditableTable",
+ "openstack-uicore-foundation/lib/components/mui/bulk-edit-table",
() =>
- function EditableTableMock(props) {
+ function BulkEditTableMock(props) {
mockEditableTableSpy(props);
return null;
}
@@ -56,56 +56,6 @@ describe("SummitEventListPage", () => {
windowOpenSpy.mockRestore();
});
- test("does not pass afterUpdate prop to EditableTable in bulk mode", () => {
- renderWithRedux(, {
- initialState: {
- currentSummitState: {
- currentSummit: {
- id: 12,
- time_zone: { name: "UTC" },
- time_zone_id: "UTC",
- selection_plans: [],
- tracks: [],
- event_types: [],
- locations: [],
- presentation_action_types: []
- }
- },
- mediaUploadListState: {
- media_uploads: []
- },
- currentEventListState: {
- events: [
- {
- id: 101,
- type: { id: 1, name: "Presentation", use_speakers: true },
- title: "Sample event",
- selection_status: "pending",
- media_uploads: []
- }
- ],
- lastPage: 1,
- currentPage: 1,
- order: "id",
- orderDir: 1,
- totalEvents: 1,
- term: "",
- filters: {},
- extraColumns: ["media_uploads"],
- perPage: 10
- }
- }
- });
-
- expect(mockEditableTableSpy).toHaveBeenCalled();
-
- const editableTableProps =
- mockEditableTableSpy.mock.calls[
- mockEditableTableSpy.mock.calls.length - 1
- ][0];
- expect(editableTableProps.afterUpdate).toBeUndefined();
- });
-
test("opens media upload material link using row event id", async () => {
renderWithRedux(, {
initialState: {
diff --git a/src/pages/events/summit-event-list-page/helpers.js b/src/pages/events/summit-event-list-page/helpers.js
new file mode 100644
index 000000000..2b0e5e285
--- /dev/null
+++ b/src/pages/events/summit-event-list-page/helpers.js
@@ -0,0 +1,1051 @@
+/**
+ * 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 T from "i18n-react/dist/i18n-react";
+import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown";
+import SpeakerInput from "openstack-uicore-foundation/lib/components/inputs/speaker-input";
+import { escapeFilterValue } from "openstack-uicore-foundation/lib/utils/actions";
+import { OPERATORS } from "openstack-uicore-foundation/lib/components/mui/grid-filter";
+import {
+ queryMembers,
+ queryTags
+} from "openstack-uicore-foundation/lib/utils/query-actions";
+import {
+ queryAllCompanies,
+ querySpeakerCompany,
+ querySubmitterCompany
+} from "../../../actions/event-actions";
+import { formatDate, formatDuration } from "../../../utils/methods";
+import { DEFAULT_Z_INDEX, HIGH_Z_INDEX } from "../../../utils/constants";
+import { buildNameIdDDL } from "../../../utils/events/summit-event-list-page.utils";
+
+// The table's column keys don't always match the API's order-by keys;
+// this is the single source of truth for that aliasing, used both when
+// sending a sort request and when highlighting the active sort column.
+const SORT_KEY_TO_API_KEY = {
+ name: "last_name",
+ submitter_company: "created_by_company",
+ progress_flags: "actions",
+ track_name: "track"
+};
+
+const API_KEY_TO_SORT_KEY = Object.fromEntries(
+ Object.entries(SORT_KEY_TO_API_KEY).map(([uiKey, apiKey]) => [apiKey, uiKey])
+);
+
+export const toApiSortKey = (key) => SORT_KEY_TO_API_KEY[key] ?? key;
+
+export const toUiSortKey = (key) => API_KEY_TO_SORT_KEY[key] ?? key;
+
+// Adds display-only fields for the table to render; never overwrites a real
+// event field, since the same row object is sent back as-is to bulkUpdateEvents.
+export const formatEventData = (e, summit) => {
+ const speakerCompanies = Array.isArray(e.speakers)
+ ? [...new Set(e.speakers.map((s) => s.company).filter(Boolean))]
+ : [];
+
+ const eventTypeCapacity = [
+ e.type?.allows_location && "Allows Location",
+ e.type?.allows_attendee_vote && "Allows Attendee Vote",
+ e.type?.allows_publishing_dates && "Allows Publishing Dates"
+ ].filter(Boolean);
+
+ const speakersCount = e.type?.use_speakers ? e.speakers?.length ?? 0 : "N/A";
+
+ return {
+ ...e,
+ created_by_fullname: e.created_by
+ ? `${e.created_by.first_name} ${e.created_by.last_name} (${e.created_by.email})`
+ : "TBD",
+ submitter_company: e.created_by ? e.created_by.company : "N/A",
+ speaker_names:
+ Array.isArray(e.speakers) && e.speakers.length > 0
+ ? e.speakers.map((s) => `${s.first_name} ${s.last_name}`).join(", ")
+ : "N/A",
+ speaker_company:
+ speakerCompanies.length > 0 ? speakerCompanies.join(", ") : "N/A",
+ speakers_count: speakersCount,
+ event_type_capacity: eventTypeCapacity.join(", "),
+ track_name: e?.track?.name ? e.track.name : "TBD",
+ sponsor:
+ Array.isArray(e.sponsors) && e.sponsors.length > 0
+ ? e.sponsors.map((s) => s.name).join(", ")
+ : "N/A",
+ progress_flags: e?.actions
+ ?.map((a) => `${a.type.label} (${a.is_completed ? "ON" : "OFF"})`)
+ .join(", "),
+ published_date_display: e.is_published
+ ? formatDate(
+ e.published_date,
+ summit.time_zone.name,
+ "MMMM Do YYYY, h:mm a"
+ )
+ : "No",
+ start_date_display: e.start_date
+ ? formatDate(e.start_date, summit.time_zone.name, "MMMM Do YYYY, h:mm a")
+ : "TBD",
+ end_date_display: e.end_date
+ ? formatDate(e.end_date, summit.time_zone.name, "MMMM Do YYYY, h:mm a")
+ : "TBD",
+ created_display: e.created
+ ? formatDate(e.created, summit.time_zone_id, "MMMM Do YYYY, h:mm a")
+ : "TBD",
+ modified: e.last_edited
+ ? formatDate(e.last_edited, summit.time_zone_id, "MMMM Do YYYY, h:mm a")
+ : "TBD"
+ };
+};
+
+export const getOptionalColumns = (
+ allSelectionPlans,
+ allTracks,
+ eventTypes,
+ currentSummitId
+) => [
+ {
+ columnKey: "speaker_names",
+ customStyle: { minWidth: "350px" },
+ label: T.translate("event_list.speakers"),
+ editableField: (extraProps) => {
+ const useSpeakers = extraProps.row.type?.use_speakers;
+ return (
+ useSpeakers && (
+ ({ ...base, zIndex: 9999 }),
+ control: (base, state) => ({
+ ...base,
+ zIndex: state.menuIsOpen ? HIGH_Z_INDEX : DEFAULT_Z_INDEX
+ })
+ }}
+ getOptionLabel={(speaker) =>
+ `${speaker.first_name} ${speaker.last_name} (${speaker.email})`
+ }
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...extraProps}
+ value={extraProps.row.speakers}
+ />
+ )
+ );
+ }
+ },
+ {
+ columnKey: "created_by_fullname",
+ sortable: true,
+ label: T.translate("event_list.created_by")
+ },
+ {
+ columnKey: "published_date",
+ sortable: true,
+ label: T.translate("event_list.published"),
+ render: (_, row) => row.published_date_display
+ },
+ {
+ columnKey: "duration",
+ sortable: true,
+ label: T.translate("event_list.duration"),
+ render: (duration, row) =>
+ row.type?.allows_publishing_dates && duration
+ ? formatDuration(duration)
+ : "N/A"
+ },
+ {
+ columnKey: "speakers_count",
+ sortable: true,
+ label: T.translate("event_list.speakers_count")
+ },
+ {
+ columnKey: "speaker_company",
+ sortable: true,
+ label: T.translate("event_list.speaker_company")
+ },
+ {
+ columnKey: "track_name",
+ sortable: true,
+ label: T.translate("event_list.track"),
+ editableField: (extraProps) => {
+ const trackOptions = buildNameIdDDL(allTracks);
+
+ return (
+ ({ ...base, zIndex: 9999 }),
+ control: (base, state) => ({
+ ...base,
+ zIndex: state.menuIsOpen ? HIGH_Z_INDEX : DEFAULT_Z_INDEX
+ })
+ }}
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...extraProps}
+ value={extraProps.row.track?.id}
+ />
+ );
+ }
+ },
+ {
+ columnKey: "start_date",
+ sortable: true,
+ label: T.translate("event_list.start_date"),
+ render: (_, row) => row.start_date_display
+ },
+ {
+ columnKey: "end_date",
+ sortable: true,
+ label: T.translate("event_list.end_date"),
+ render: (_, row) => row.end_date_display
+ },
+ {
+ columnKey: "submitter_company",
+ sortable: true,
+ label: T.translate("event_list.submitter_company")
+ },
+ {
+ columnKey: "sponsor",
+ sortable: true,
+ label: T.translate("event_list.sponsor")
+ },
+ {
+ columnKey: "event_type_capacity",
+ label: T.translate("event_list.event_type_capacity")
+ },
+ {
+ columnKey: "selection_plan",
+ sortable: true,
+ label: T.translate("event_list.selection_plan"),
+ editableField: (extraProps) => {
+ if (!extraProps.row?.type?.id) return false;
+ const eventType = Array.isArray(eventTypes)
+ ? eventTypes.find(
+ (t) => t?.id !== undefined && t.id === extraProps.row.type?.id
+ )
+ : null;
+ if (!eventType) return false;
+
+ const allowSelectionPlanEdit =
+ ["PresentationType"].includes(eventType.class_name) ||
+ ["PresentationType"].includes(eventType.name);
+ if (!allowSelectionPlanEdit) return false;
+
+ const trackId = extraProps.row?.track?.id;
+ const track =
+ trackId !== undefined && trackId !== null
+ ? allTracks.find((t) => t?.id !== undefined && t.id === trackId)
+ : null;
+
+ const selectionPlansPerTrack = buildNameIdDDL(
+ (Array.isArray(allSelectionPlans) ? allSelectionPlans : []).filter(
+ (sp) =>
+ !track ||
+ (Array.isArray(sp.track_groups) &&
+ Array.isArray(track.track_groups) &&
+ sp.track_groups.some((gr) => track.track_groups.includes(gr)))
+ )
+ );
+
+ return (
+ ({ ...base, zIndex: 9999 }),
+ control: (base, state) => ({
+ ...base,
+ width: 220,
+ zIndex: state.menuIsOpen ? HIGH_Z_INDEX : DEFAULT_Z_INDEX
+ })
+ }}
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...extraProps}
+ value={extraProps.value || ""}
+ />
+ );
+ },
+ render: (e) => (e?.name ? e.name : "N/A")
+ },
+ {
+ columnKey: "location",
+ sortable: true,
+ label: T.translate("event_list.location"),
+ render: (location) => (location?.name ? location.name : "N/A")
+ },
+ {
+ columnKey: "level",
+ sortable: true,
+ label: T.translate("event_list.level"),
+ render: (level) => level || "N/A"
+ },
+ {
+ columnKey: "tags",
+ sortable: true,
+ label: T.translate("event_list.tags"),
+ render: (tags) =>
+ Array.isArray(tags) && tags.length > 0
+ ? tags.reduce(
+ (accumulator, t) =>
+ accumulator + (accumulator !== "" ? ", " : "") + t.tag,
+ ""
+ )
+ : "N/A"
+ },
+ {
+ columnKey: "streaming_url",
+ sortable: true,
+ title: true,
+ editableField: true,
+ label: T.translate("event_list.streaming_url"),
+ placeholder: T.translate("bulk_actions_page.placeholders.streaming_url"),
+ render: (url) => url || "N/A"
+ },
+ {
+ columnKey: "meeting_url",
+ sortable: true,
+ title: true,
+ editableField: true,
+ label: T.translate("event_list.meeting_url"),
+ placeholder: T.translate("bulk_actions_page.placeholders.meeting_url"),
+ render: (url) => url || "N/A"
+ },
+ {
+ columnKey: "etherpad_link",
+ sortable: true,
+ title: true,
+ editableField: true,
+ label: T.translate("event_list.etherpad_link"),
+ placeholder: T.translate("bulk_actions_page.placeholders.etherpad_link"),
+ render: (link) => link || "N/A"
+ },
+ {
+ columnKey: "streaming_type",
+ sortable: true,
+ label: T.translate("event_list.streaming_type"),
+ render: (type) => type || "N/A"
+ },
+ {
+ columnKey: "review_status",
+ sortable: true,
+ title: true,
+ label: T.translate("event_list.review_status"),
+ render: (status) => status ?? "N/A"
+ },
+ {
+ columnKey: "status",
+ sortable: true,
+ title: true,
+ label: T.translate("event_list.submission_status"),
+ render: (status) => status ?? "Not Submitted"
+ },
+ {
+ columnKey: "progress_flags",
+ sortable: true,
+ title: true,
+ label: T.translate("event_list.progress_flags")
+ },
+ {
+ columnKey: "created",
+ sortable: true,
+ label: T.translate("event_list.created"),
+ render: (_, row) => row.created_display
+ },
+ {
+ columnKey: "modified",
+ sortable: true,
+ label: T.translate("event_list.modified")
+ },
+ {
+ columnKey: "submission_source",
+ sortable: true,
+ label: T.translate("event_list.submission_source"),
+ render: (source) => source || "N/A"
+ },
+ {
+ columnKey: "media_uploads",
+ sortable: false,
+ label: T.translate("event_list.media_uploads"),
+ render: (e, row) => {
+ if (!e?.length) return "N/A";
+ return (
+ <>
+ {e.map((m) => (
+
+
+
+
+ ))}
+ >
+ );
+ }
+ },
+ {
+ columnKey: "media_uploads_display",
+ sortable: false,
+ label: T.translate("event_list.media_uploads_display"),
+ render: (e, row) => {
+ const mediaUploads = row?.media_uploads || [];
+ if (!mediaUploads.length) return "N/A";
+ return (
+ <>
+ {mediaUploads.map((m) => (
+
+ {`"${m.media_upload_type.name}" : `}
+ {`${m.display_on_site ? "Yes" : "No"}`}
+
+
+ ))}
+ >
+ );
+ }
+ },
+ {
+ columnKey: "allow_feedback",
+ sortable: false,
+ label: T.translate("event_list.allow_feedback"),
+ render: (field) =>
+ field === undefined ? "N/A" : field === true ? "Yes" : "No",
+ editableField: (extraProps) => (
+ ({ ...base, zIndex: 9999 }),
+ control: (base, state) => ({
+ ...base,
+ zIndex: state.menuIsOpen ? HIGH_Z_INDEX : DEFAULT_Z_INDEX
+ })
+ }}
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...extraProps}
+ />
+ )
+ },
+ {
+ columnKey: "to_record",
+ sortable: false,
+ label: T.translate("event_list.to_record"),
+ render: (field) =>
+ field === undefined ? "N/A" : field === true ? "Yes" : "No",
+ editableField: (extraProps) => (
+ ({ ...base, zIndex: 9999 }),
+ control: (base, state) => ({
+ ...base,
+ zIndex: state.menuIsOpen ? HIGH_Z_INDEX : DEFAULT_Z_INDEX
+ })
+ }}
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...extraProps}
+ />
+ )
+ }
+];
+
+const eventTypeCapacityOptions = [
+ {
+ value: "allows_attendee_vote_filter",
+ label: T.translate("event_list.allows_attendee_vote_filter")
+ },
+ {
+ value: "allows_location_filter",
+ label: T.translate("event_list.allows_location_filter")
+ },
+ {
+ value: "allows_publishing_dates_filter",
+ label: T.translate("event_list.allows_publishing_dates_filter")
+ }
+];
+
+const levelOptions = [
+ { label: "Beginner", value: "beginner" },
+ { label: "Intermediate", value: "intermediate" },
+ { label: "Advanced", value: "advanced" },
+ { label: "N/A", value: "na" }
+];
+
+const selectionStatusOptions = [
+ { label: "Pending", value: "pending" },
+ { label: "Accepted", value: "accepted" },
+ { label: "Rejected", value: "rejected" },
+ { label: "Alternate", value: "alternate" }
+];
+
+const publishedStatusOptions = [
+ {
+ label: "Published",
+ value: "1"
+ },
+ {
+ label: "Non Published",
+ value: "0"
+ }
+];
+
+const rsvpOptions = [
+ {
+ label: "Has RSVP",
+ value: true
+ },
+ {
+ label: "No RSVP",
+ value: false
+ }
+];
+
+const streamingTypeOptions = [
+ { label: "LIVE", value: "LIVE" },
+ { label: "VOD", value: "VOD" }
+];
+
+const submissionStatusOptions = [
+ {
+ label: T.translate("event_list.submission_status_accepted"),
+ value: "Accepted"
+ },
+ {
+ label: T.translate("event_list.submission_status_received"),
+ value: "Received"
+ },
+ {
+ label: T.translate("event_list.submission_status_not_submitted"),
+ value: "NonReceived"
+ }
+];
+
+const reviewStatusOptions = [
+ {
+ label: T.translate("event_list.review_status_accepted"),
+ value: "Accepted"
+ },
+ {
+ label: T.translate("event_list.review_status_in_review"),
+ value: "InReview"
+ },
+ {
+ label: T.translate("event_list.review_status_no_submitted"),
+ value: "NotSubmitted"
+ },
+ {
+ label: T.translate("event_list.review_status_published"),
+ value: "Published"
+ },
+ {
+ label: T.translate("event_list.review_status_received"),
+ value: "Received"
+ },
+ {
+ label: T.translate("event_list.review_status_rejected"),
+ value: "Rejected"
+ }
+];
+
+const submissionSourceOptions = [
+ { label: "Admin", value: "Admin" },
+ { label: "Submission", value: "Submission" }
+];
+
+// queryTags' response shape differs depending on whether it's summit-scoped
+// (nested `tag.tag`) or global (flat `tag`); handle both defensively.
+const tagFormatOption = (item) => ({
+ value: item.id,
+ label:
+ typeof item.tag === "string" ? item.tag : item.tag?.tag || String(item.id)
+});
+
+export const getCriterias = (summit, mediaUploadTypes) => [
+ {
+ key: "event_type_capacity",
+ label: T.translate("event_list.event_type_capacity"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: eventTypeCapacityOptions,
+ multiple: true
+ }
+ },
+ customParser: (f) => {
+ const filter = [];
+
+ if (f.value.includes("allows_attendee_vote_filter")) {
+ filter.push("type_allows_attendee_vote==1");
+ }
+ if (f.value.includes("allows_location_filter")) {
+ filter.push("type_allows_location==1");
+ }
+ if (f.value.includes("allows_publishing_dates_filter")) {
+ filter.push("type_allows_publishing_dates==1");
+ }
+
+ return filter;
+ }
+ },
+ {
+ key: "selection_plan_id",
+ label: T.translate("event_list.selection_plan"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: summit.selection_plans.map((sp) => ({
+ label: sp.name,
+ value: sp.id
+ })),
+ multiple: true
+ }
+ }
+ },
+ {
+ key: "location_id",
+ label: T.translate("event_list.location"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: summit.locations.map((sp) => ({
+ label: sp.name,
+ value: sp.id
+ })),
+ multiple: true
+ }
+ }
+ },
+ {
+ key: "selection_status",
+ label: "Selection Status",
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: selectionStatusOptions,
+ multiple: true
+ }
+ }
+ },
+ {
+ key: "track_id",
+ label: T.translate("event_list.track"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: summit.tracks.map((t) => ({ label: t.name, value: t.id })),
+ multiple: true
+ }
+ }
+ },
+ {
+ key: "event_type_id",
+ label: "Activity Type",
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: summit.event_types.map((type) => ({
+ label: type.name,
+ value: type.id
+ })),
+ multiple: true
+ }
+ }
+ },
+ {
+ key: "speaker_id",
+ label: T.translate("event_list.speakers"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "speaker",
+ props: {
+ summitId: summit.id,
+ multiple: true
+ }
+ },
+ customParser: (f) => [
+ `speaker_id==${f.value.map((s) => s.value).join("||")}`
+ ]
+ },
+ {
+ key: "speaker_company",
+ label: T.translate("event_list.speaker_company"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "company",
+ props: {
+ queryFunction: querySpeakerCompany,
+ multiple: true
+ }
+ },
+ customParser: (f) => [
+ `speaker_company==${f.value
+ .map((c) => escapeFilterValue(c.raw.name))
+ .join("||")}`
+ ]
+ },
+ {
+ key: "level",
+ label: T.translate("event_list.level"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: levelOptions,
+ multiple: true
+ }
+ }
+ },
+ {
+ key: "tags",
+ label: T.translate("event_list.tags"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "asyncSelect",
+ props: {
+ queryFunction: (input, callback) =>
+ queryTags(summit.id, input, callback),
+ formatOption: tagFormatOption,
+ multiple: true
+ }
+ },
+ customParser: (f) => [
+ `tags==${f.value.map((t) => escapeFilterValue(t.label)).join("||")}`
+ ]
+ },
+ {
+ key: "published",
+ label: T.translate("event_list.published"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: publishedStatusOptions
+ }
+ }
+ },
+ {
+ key: "rsvp_type",
+ label: "Has RSVP?",
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: rsvpOptions
+ }
+ },
+ customParser: (f) => [`rsvp_type${f.value ? "<>" : "=="}None`]
+ },
+ {
+ key: "progress_flag",
+ label: T.translate("event_list.progress_flags"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: summit.presentation_action_types.map((pf) => ({
+ value: pf.id,
+ label: pf.label
+ })),
+ multiple: true
+ }
+ },
+ customParser: (f) => [
+ f.value.map((pf) => `actions==type_id==${pf}&&is_completed==1`).join(",")
+ ]
+ },
+ {
+ key: "created",
+ label: T.translate("event_list.created"),
+ operators: [OPERATORS.BEFORE, OPERATORS.AFTER],
+ values: {
+ type: "datetime",
+ props: {
+ mode: "datetime"
+ }
+ }
+ },
+ {
+ key: "last_edited",
+ label: T.translate("event_list.modified"),
+ operators: [OPERATORS.BEFORE, OPERATORS.AFTER],
+ values: {
+ type: "datetime",
+ props: {
+ mode: "datetime"
+ }
+ }
+ },
+ {
+ key: "start_date",
+ label: T.translate("event_list.start_date"),
+ operators: [OPERATORS.BEFORE, OPERATORS.AFTER],
+ values: {
+ type: "datetime",
+ props: {
+ mode: "datetime"
+ }
+ }
+ },
+ {
+ key: "end_date",
+ label: T.translate("event_list.end_date"),
+ operators: [OPERATORS.BEFORE, OPERATORS.AFTER],
+ values: {
+ type: "datetime",
+ props: {
+ mode: "datetime"
+ }
+ }
+ },
+ {
+ key: "duration",
+ label: T.translate("event_list.duration"),
+ operators: [OPERATORS.IS, OPERATORS.LESS, OPERATORS.GREATER],
+ values: {
+ type: "number",
+ props: {
+ min: 0,
+ integer: true
+ }
+ }
+ },
+ {
+ key: "speakers_count",
+ label: T.translate("event_list.speakers_count"),
+ operators: [OPERATORS.IS, OPERATORS.LESS, OPERATORS.GREATER],
+ values: {
+ type: "number",
+ props: {
+ min: 0,
+ integer: true
+ }
+ }
+ },
+ {
+ key: "submitters",
+ label: "Submitters",
+ operators: [OPERATORS.IS],
+ values: {
+ type: "asyncSelect",
+ props: {
+ queryFunction: queryMembers,
+ formatOption: (m) => ({
+ value: m.id,
+ label: `${m.first_name} ${m.last_name} (${m.email})`
+ }),
+ multiple: true
+ }
+ },
+ customParser: (f) => [
+ f.value
+ .flatMap((s) => {
+ const escapedFullName = escapeFilterValue(
+ `${s.raw.first_name} ${s.raw.last_name}`
+ );
+ const escapedEmail = escapeFilterValue(s.raw.email);
+ const fullNameFilter = `created_by_fullname==${escapedFullName}`;
+ const emailFilter = `created_by_email==${escapedEmail}`;
+ return [fullNameFilter, emailFilter];
+ })
+ .join(",")
+ ]
+ },
+ {
+ key: "created_by_company",
+ label: T.translate("event_list.submitter_company"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "company",
+ props: {
+ queryFunction: querySubmitterCompany,
+ multiple: true
+ }
+ },
+ customParser: (f) => [
+ `created_by_company==${f.value
+ .map((c) => escapeFilterValue(c.raw.name))
+ .join("||")}`
+ ]
+ },
+ {
+ key: "streaming_url",
+ label: T.translate("event_list.streaming_url"),
+ operators: [OPERATORS.IS, OPERATORS.LIKE_START, OPERATORS.LIKE],
+ values: {
+ type: "text",
+ props: {}
+ }
+ },
+ {
+ key: "meeting_url",
+ label: T.translate("event_list.meeting_url"),
+ operators: [OPERATORS.IS, OPERATORS.LIKE_START, OPERATORS.LIKE],
+ values: {
+ type: "text",
+ props: {}
+ }
+ },
+ {
+ key: "etherpad_link",
+ label: T.translate("event_list.etherpad_link"),
+ operators: [OPERATORS.IS, OPERATORS.LIKE_START, OPERATORS.LIKE],
+ values: {
+ type: "text",
+ props: {}
+ }
+ },
+ {
+ key: "streaming_type",
+ label: T.translate("event_list.streaming_type"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: streamingTypeOptions
+ }
+ }
+ },
+ {
+ key: "sponsor",
+ label: T.translate("event_list.sponsor"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "company",
+ props: {
+ multiple: true
+ }
+ },
+ customParser: (f) => [
+ `sponsor==${f.value.map((s) => escapeFilterValue(s.raw.name)).join("||")}`
+ ]
+ },
+ {
+ key: "all_companies",
+ label: "All Companies",
+ operators: [OPERATORS.IS],
+ values: {
+ type: "company",
+ props: {
+ queryFunction: queryAllCompanies,
+ multiple: true
+ }
+ },
+ customParser: (f) => {
+ const companies = f.value
+ .map((c) => escapeFilterValue(c.raw.name))
+ .join("||");
+ return [
+ `speaker_company==${companies},created_by_company==${companies},sponsor==${companies}`
+ ];
+ }
+ },
+ {
+ key: "submission_status",
+ label: T.translate("event_list.submission_status"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: submissionStatusOptions
+ }
+ }
+ },
+ {
+ key: "media_upload_with_type",
+ label: "Media Upload Type",
+ operators: [OPERATORS.HAS, OPERATORS.HAS_NOT],
+ values: {
+ type: "select",
+ props: {
+ options: mediaUploadTypes.map((type) => ({
+ value: type.id,
+ label: type.name
+ })),
+ multiple: true
+ }
+ },
+ customParser: (f) => {
+ const filter = [];
+
+ if (f.operator === OPERATORS.HAS.value) {
+ const value = Array.isArray(f.value) ? f.value.join("||") : f.value;
+ filter.push(`has_media_upload_with_type==${value}`);
+ } else {
+ const value = Array.isArray(f.value) ? f.value.join("&&") : f.value;
+ filter.push(`has_not_media_upload_with_type==${value}`);
+ }
+
+ return filter;
+ }
+ },
+ {
+ key: "review_status",
+ label: T.translate("event_list.review_status"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: reviewStatusOptions,
+ multiple: true
+ }
+ }
+ },
+ {
+ key: "submission_source",
+ label: T.translate("event_list.submission_source"),
+ operators: [OPERATORS.IS],
+ values: {
+ type: "select",
+ props: {
+ options: submissionSourceOptions
+ }
+ }
+ }
+];
diff --git a/src/pages/events/summit-event-list-page/index.js b/src/pages/events/summit-event-list-page/index.js
index 73c9a21ab..f5a874dfc 100644
--- a/src/pages/events/summit-event-list-page/index.js
+++ b/src/pages/events/summit-event-list-page/index.js
@@ -15,20 +15,13 @@ import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import T from "i18n-react/dist/i18n-react";
import Swal from "sweetalert2";
-import { Pagination } from "react-bootstrap";
import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown";
import FreeTextSearch from "openstack-uicore-foundation/lib/components/free-text-search";
-import SpeakerInput from "openstack-uicore-foundation/lib/components/inputs/speaker-input";
-import { escapeFilterValue } from "openstack-uicore-foundation/lib/utils/actions";
import {
GridFilter,
- OPERATORS,
useGridFilter
} from "openstack-uicore-foundation/lib/components/mui/grid-filter";
-import {
- queryMembers,
- queryTags
-} from "openstack-uicore-foundation/lib/utils/query-actions";
+import BulkEditTable from "openstack-uicore-foundation/lib/components/mui/bulk-edit-table";
import {
bulkUpdateEvents,
changeEventListSearchTerm,
@@ -36,10 +29,7 @@ import {
exportEvents,
getEvents,
importEventsCSV,
- importMP4AssetsFromMUX,
- queryAllCompanies,
- querySpeakerCompany,
- querySubmitterCompany
+ importMP4AssetsFromMUX
} from "../../../actions/event-actions";
import { getMediaUploads } from "../../../actions/media-upload-actions";
import { handleDDLSortByLabel } from "../../../utils/methods";
@@ -49,11 +39,6 @@ import {
HIGH_Z_INDEX,
MAX_PER_PAGE
} from "../../../utils/constants";
-import {
- defaultColumns,
- editableColumns,
- formatEventData
-} from "../../../utils/summitUtils";
import SaveFilterCriteria from "../../../components/filters/save-filter-criteria";
import SelectFilterCriteria from "../../../components/filters/select-filter-criteria";
import {
@@ -61,870 +46,19 @@ import {
saveFilterCriteria
} from "../../../actions/filter-criteria-actions";
import { CONTEXT_ACTIVITIES } from "../../../utils/filter-criteria-constants";
-import EditableTable from "../../../components/tables/editable-table/EditableTable";
import { buildNameIdDDL } from "../../../utils/events/summit-event-list-page.utils";
import ImportModal from "./components/ImportModal";
import ImportMUXModal from "./components/ImportMUXModal";
+import {
+ formatEventData,
+ getCriterias,
+ getOptionalColumns,
+ toApiSortKey,
+ toUiSortKey
+} from "./helpers";
const FILTER_ID = "summit_activity_list";
-const fieldNames = (
- allSelectionPlans,
- allTracks,
- eventTypes,
- currentSummitId
-) => [
- {
- columnKey: "speakers",
- value: "speakers",
- customStyle: { minWidth: "350px" },
- editableField: (extraProps) => {
- const useSpeakers = extraProps.row.type?.use_speakers;
- return (
- useSpeakers && (
- ({ ...base, zIndex: 9999 }),
- control: (base, state) => ({
- ...base,
- zIndex: state.menuIsOpen ? HIGH_Z_INDEX : DEFAULT_Z_INDEX
- })
- }}
- getOptionLabel={(speaker) =>
- `${speaker.first_name} ${speaker.last_name} (${speaker.email})`
- }
- // eslint-disable-next-line react/jsx-props-no-spreading
- {...extraProps}
- />
- )
- );
- }
- },
- { columnKey: "created_by_fullname", value: "created_by", sortable: true },
- { columnKey: "published_date", value: "published", sortable: true },
- { columnKey: "duration", value: "duration", sortable: true },
- { columnKey: "speakers_count", value: "speakers_count", sortable: true },
- { columnKey: "speaker_company", value: "speaker_company", sortable: true },
- {
- columnKey: "track",
- value: "track",
- sortable: true,
- editableField: (extraProps) => {
- const trackOptions = buildNameIdDDL(allTracks);
-
- return (
- ({ ...base, zIndex: 9999 }),
- control: (base, state) => ({
- ...base,
- zIndex: state.menuIsOpen ? HIGH_Z_INDEX : DEFAULT_Z_INDEX
- })
- }}
- // eslint-disable-next-line react/jsx-props-no-spreading
- {...extraProps}
- />
- );
- }
- },
- { columnKey: "start_date", value: "start_date", sortable: true },
- { columnKey: "end_date", value: "end_date", sortable: true },
- { columnKey: "submitters", value: "submitters" },
- {
- columnKey: "submitter_company",
- value: "submitter_company",
- sortable: true
- },
- { columnKey: "sponsor", value: "sponsor", sortable: true },
- { columnKey: "event_type_capacity", value: "event_type_capacity" },
- {
- columnKey: "selection_plan",
- value: "selection_plan",
- sortable: true,
- editableField: (extraProps) => {
- if (!extraProps.row?.type?.id) return false;
- const eventType = Array.isArray(eventTypes)
- ? eventTypes.find(
- (t) => t?.id !== undefined && t.id === extraProps.row.type?.id
- )
- : null;
- if (!eventType) return false;
-
- const allowSelectionPlanEdit =
- ["PresentationType"].includes(eventType.class_name) ||
- ["PresentationType"].includes(eventType.name);
- if (!allowSelectionPlanEdit) return false;
-
- const trackId = extraProps.row?.track?.id;
- const track =
- trackId !== undefined && trackId !== null
- ? allTracks.find((t) => t?.id !== undefined && t.id === trackId)
- : null;
-
- const selectionPlansPerTrack = buildNameIdDDL(
- (Array.isArray(allSelectionPlans) ? allSelectionPlans : []).filter(
- (sp) =>
- !track ||
- (Array.isArray(sp.track_groups) &&
- Array.isArray(track.track_groups) &&
- sp.track_groups.some((gr) => track.track_groups.includes(gr)))
- )
- );
-
- return (
- ({ ...base, zIndex: 9999 }),
- control: (base, state) => ({
- ...base,
- width: 220,
- zIndex: state.menuIsOpen ? HIGH_Z_INDEX : DEFAULT_Z_INDEX
- })
- }}
- // eslint-disable-next-line react/jsx-props-no-spreading
- {...extraProps}
- />
- );
- },
- render: (e) => (e?.name ? e.name : "N/A")
- },
- { columnKey: "location", value: "location", sortable: true },
- { columnKey: "level", value: "level", sortable: true },
- { columnKey: "tags", value: "tags", sortable: true },
- {
- columnKey: "streaming_url",
- value: "streaming_url",
- sortable: true,
- title: true,
- editableField: true
- },
- {
- columnKey: "meeting_url",
- value: "meeting_url",
- sortable: true,
- title: true,
- editableField: true
- },
- {
- columnKey: "etherpad_link",
- value: "etherpad_link",
- sortable: true,
- title: true,
- editableField: true
- },
- { columnKey: "streaming_type", value: "streaming_type", sortable: true },
- {
- columnKey: "review_status",
- value: "review_status",
- sortable: true,
- title: true
- },
- {
- columnKey: "status",
- value: "submission_status",
- sortable: true,
- title: true
- },
- {
- columnKey: "progress_flags",
- value: "progress_flags",
- sortable: true,
- title: true
- },
- { columnKey: "created", value: "created", sortable: true },
- { columnKey: "modified", value: "modified", sortable: true },
- {
- columnKey: "submission_source",
- value: "submission_source",
- sortable: true
- },
- {
- columnKey: "media_uploads",
- value: "media_uploads",
- sortable: false,
- render: (e, row) => {
- if (!e?.length) return "N/A";
- return (
- <>
- {e.map((m) => (
-
-
-
-
- ))}
- >
- );
- }
- },
- {
- columnKey: "media_uploads_display",
- value: "media_uploads_display",
- sortable: false,
- render: (e, row) => {
- const mediaUploads = row?.media_uploads || [];
- if (!mediaUploads.length) return "N/A";
- return (
- <>
- {mediaUploads.map((m) => (
-
- {`"${m.media_upload_type.name}" : `}
- {`${m.display_on_site ? "Yes" : "No"}`}
-
-
- ))}
- >
- );
- }
- },
- {
- columnKey: "allow_feedback",
- value: "allow_feedback",
- sortable: false,
- render: (field) =>
- field === undefined ? "N/A" : field === true ? "Yes" : "No",
- editableField: (extraProps) => (
- ({ ...base, zIndex: 9999 }),
- control: (base, state) => ({
- ...base,
- zIndex: state.menuIsOpen ? HIGH_Z_INDEX : DEFAULT_Z_INDEX
- })
- }}
- // eslint-disable-next-line react/jsx-props-no-spreading
- {...extraProps}
- />
- )
- },
- {
- columnKey: "to_record",
- value: "to_record",
- sortable: false,
- render: (field) =>
- field === undefined ? "N/A" : field === true ? "Yes" : "No",
- editableField: (extraProps) => (
- ({ ...base, zIndex: 9999 }),
- control: (base, state) => ({
- ...base,
- zIndex: state.menuIsOpen ? HIGH_Z_INDEX : DEFAULT_Z_INDEX
- })
- }}
- // eslint-disable-next-line react/jsx-props-no-spreading
- {...extraProps}
- />
- )
- }
-];
-
-const eventTypeCapacityOptions = [
- {
- value: "allows_attendee_vote_filter",
- label: T.translate("event_list.allows_attendee_vote_filter")
- },
- {
- value: "allows_location_filter",
- label: T.translate("event_list.allows_location_filter")
- },
- {
- value: "allows_publishing_dates_filter",
- label: T.translate("event_list.allows_publishing_dates_filter")
- }
-];
-
-const levelOptions = [
- { label: "Beginner", value: "beginner" },
- { label: "Intermediate", value: "intermediate" },
- { label: "Advanced", value: "advanced" },
- { label: "N/A", value: "na" }
-];
-
-const selectionStatusOptions = [
- { label: "Pending", value: "pending" },
- { label: "Accepted", value: "accepted" },
- { label: "Rejected", value: "rejected" },
- { label: "Alternate", value: "alternate" }
-];
-
-const publishedStatusOptions = [
- {
- label: "Published",
- value: "1"
- },
- {
- label: "Non Published",
- value: "0"
- }
-];
-
-const rsvpOptions = [
- {
- label: "Has RSVP",
- value: true
- },
- {
- label: "No RSVP",
- value: false
- }
-];
-
-const streamingTypeOptions = [
- { label: "LIVE", value: "LIVE" },
- { label: "VOD", value: "VOD" }
-];
-
-const submissionStatusOptions = [
- {
- label: T.translate("event_list.submission_status_accepted"),
- value: "Accepted"
- },
- {
- label: T.translate("event_list.submission_status_received"),
- value: "Received"
- },
- {
- label: T.translate("event_list.submission_status_not_submitted"),
- value: "NonReceived"
- }
-];
-
-const reviewStatusOptions = [
- {
- label: T.translate("event_list.review_status_accepted"),
- value: "Accepted"
- },
- {
- label: T.translate("event_list.review_status_in_review"),
- value: "InReview"
- },
- {
- label: T.translate("event_list.review_status_no_submitted"),
- value: "NotSubmitted"
- },
- {
- label: T.translate("event_list.review_status_published"),
- value: "Published"
- },
- {
- label: T.translate("event_list.review_status_received"),
- value: "Received"
- },
- {
- label: T.translate("event_list.review_status_rejected"),
- value: "Rejected"
- }
-];
-
-const submissionSourceOptions = [
- { label: "Admin", value: "Admin" },
- { label: "Submission", value: "Submission" }
-];
-
-// queryTags' response shape differs depending on whether it's summit-scoped
-// (nested `tag.tag`) or global (flat `tag`); handle both defensively.
-const tagFormatOption = (item) => ({
- value: item.id,
- label:
- typeof item.tag === "string" ? item.tag : item.tag?.tag || String(item.id)
-});
-
-const getCriterias = (summit, mediaUploadTypes) => [
- {
- key: "event_type_capacity",
- label: "Activity Type Capacity",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: eventTypeCapacityOptions,
- multiple: true
- }
- },
- customParser: (f) => {
- const filter = [];
-
- if (f.value.includes("allows_attendee_vote_filter")) {
- filter.push("type_allows_attendee_vote==1");
- }
- if (f.value.includes("allows_location_filter")) {
- filter.push("type_allows_location==1");
- }
- if (f.value.includes("allows_publishing_dates_filter")) {
- filter.push("type_allows_publishing_dates==1");
- }
-
- return filter;
- }
- },
- {
- key: "selection_plan_id",
- label: "Selection Plan",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: summit.selection_plans.map((sp) => ({
- label: sp.name,
- value: sp.id
- })),
- multiple: true
- }
- }
- },
- {
- key: "location_id",
- label: "Location",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: summit.locations.map((sp) => ({
- label: sp.name,
- value: sp.id
- })),
- multiple: true
- }
- }
- },
- {
- key: "selection_status",
- label: "Selection Status",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: selectionStatusOptions,
- multiple: true
- }
- }
- },
- {
- key: "track_id",
- label: "Activity Category",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: summit.tracks.map((t) => ({ label: t.name, value: t.id })),
- multiple: true
- }
- }
- },
- {
- key: "event_type_id",
- label: "Activity Type",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: summit.event_types.map((type) => ({
- label: type.name,
- value: type.id
- })),
- multiple: true
- }
- }
- },
- {
- key: "speaker_id",
- label: "Speakers",
- operators: [OPERATORS.IS],
- values: {
- type: "speaker",
- props: {
- summitId: summit.id,
- multiple: true
- }
- },
- customParser: (f) => [
- `speaker_id==${f.value.map((s) => s.value).join("||")}`
- ]
- },
- {
- key: "speaker_company",
- label: "Speaker Company",
- operators: [OPERATORS.IS],
- values: {
- type: "company",
- props: {
- queryFunction: querySpeakerCompany,
- multiple: true
- }
- },
- customParser: (f) => [
- `speaker_company==${f.value
- .map((c) => escapeFilterValue(c.raw.name))
- .join("||")}`
- ]
- },
- {
- key: "level",
- label: "Activity Level",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: levelOptions,
- multiple: true
- }
- }
- },
- {
- key: "tags",
- label: "Tags",
- operators: [OPERATORS.IS],
- values: {
- type: "asyncSelect",
- props: {
- queryFunction: (input, callback) =>
- queryTags(summit.id, input, callback),
- formatOption: tagFormatOption,
- multiple: true
- }
- },
- customParser: (f) => [
- `tags==${f.value.map((t) => escapeFilterValue(t.label)).join("||")}`
- ]
- },
- {
- key: "published",
- label: "Published",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: publishedStatusOptions
- }
- }
- },
- {
- key: "rsvp_type",
- label: "Has RSVP?",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: rsvpOptions
- }
- },
- customParser: (f) => [`rsvp_type${f.value ? "<>" : "=="}None`]
- },
- {
- key: "progress_flag",
- label: "Progress Flag",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: summit.presentation_action_types.map((pf) => ({
- value: pf.id,
- label: pf.label
- })),
- multiple: true
- }
- },
- customParser: (f) => [
- f.value.map((pf) => `actions==type_id==${pf}&&is_completed==1`).join(",")
- ]
- },
- {
- key: "created",
- label: "Created",
- operators: [OPERATORS.BEFORE, OPERATORS.AFTER],
- values: {
- type: "datetime",
- props: {
- mode: "datetime"
- }
- }
- },
- {
- key: "last_edited",
- label: "Modified",
- operators: [OPERATORS.BEFORE, OPERATORS.AFTER],
- values: {
- type: "datetime",
- props: {
- mode: "datetime"
- }
- }
- },
- {
- key: "start_date",
- label: "Start Date",
- operators: [OPERATORS.BEFORE, OPERATORS.AFTER],
- values: {
- type: "datetime",
- props: {
- mode: "datetime"
- }
- }
- },
- {
- key: "end_date",
- label: "End Date",
- operators: [OPERATORS.BEFORE, OPERATORS.AFTER],
- values: {
- type: "datetime",
- props: {
- mode: "datetime"
- }
- }
- },
- {
- key: "duration",
- label: "Duration",
- operators: [OPERATORS.IS, OPERATORS.LESS, OPERATORS.GREATER],
- values: {
- type: "number",
- props: {
- min: 0,
- integer: true
- }
- }
- },
- {
- key: "speakers_count",
- label: "Speaker Count",
- operators: [OPERATORS.IS, OPERATORS.LESS, OPERATORS.GREATER],
- values: {
- type: "number",
- props: {
- min: 0,
- integer: true
- }
- }
- },
- {
- key: "submitters",
- label: "Submitters",
- operators: [OPERATORS.IS],
- values: {
- type: "asyncSelect",
- props: {
- queryFunction: queryMembers,
- formatOption: (m) => ({
- value: m.id,
- label: `${m.first_name} ${m.last_name} (${m.email})`
- }),
- multiple: true
- }
- },
- customParser: (f) => [
- f.value
- .flatMap((s) => {
- const escapedFullName = escapeFilterValue(
- `${s.raw.first_name} ${s.raw.last_name}`
- );
- const escapedEmail = escapeFilterValue(s.raw.email);
- const fullNameFilter = `created_by_fullname==${escapedFullName}`;
- const emailFilter = `created_by_email==${escapedEmail}`;
- return [fullNameFilter, emailFilter];
- })
- .join(",")
- ]
- },
- {
- key: "created_by_company",
- label: "Submitter Company",
- operators: [OPERATORS.IS],
- values: {
- type: "company",
- props: {
- queryFunction: querySubmitterCompany,
- multiple: true
- }
- },
- customParser: (f) => [
- `created_by_company==${f.value
- .map((c) => escapeFilterValue(c.raw.name))
- .join("||")}`
- ]
- },
- {
- key: "streaming_url",
- label: "Streaming URL",
- operators: [OPERATORS.IS, OPERATORS.LIKE_START, OPERATORS.LIKE],
- values: {
- type: "text",
- props: {}
- }
- },
- {
- key: "meeting_url",
- label: "Meeting URL",
- operators: [OPERATORS.IS, OPERATORS.LIKE_START, OPERATORS.LIKE],
- values: {
- type: "text",
- props: {}
- }
- },
- {
- key: "etherpad_link",
- label: "Etherpad Link",
- operators: [OPERATORS.IS, OPERATORS.LIKE_START, OPERATORS.LIKE],
- values: {
- type: "text",
- props: {}
- }
- },
- {
- key: "streaming_type",
- label: "Streaming Type",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: streamingTypeOptions
- }
- }
- },
- {
- key: "sponsor",
- label: "Sponsor",
- operators: [OPERATORS.IS],
- values: {
- type: "company",
- props: {
- multiple: true
- }
- },
- customParser: (f) => [
- `sponsor==${f.value.map((s) => escapeFilterValue(s.raw.name)).join("||")}`
- ]
- },
- {
- key: "all_companies",
- label: "All Companies",
- operators: [OPERATORS.IS],
- values: {
- type: "company",
- props: {
- queryFunction: queryAllCompanies,
- multiple: true
- }
- },
- customParser: (f) => {
- const companies = f.value
- .map((c) => escapeFilterValue(c.raw.name))
- .join("||");
- return [
- `speaker_company==${companies},created_by_company==${companies},sponsor==${companies}`
- ];
- }
- },
- {
- key: "submission_status",
- label: "Submitter Company",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: submissionStatusOptions
- }
- }
- },
- {
- key: "media_upload_with_type",
- label: "Media Upload Type",
- operators: [OPERATORS.HAS, OPERATORS.HAS_NOT],
- values: {
- type: "select",
- props: {
- options: mediaUploadTypes.map((type) => ({
- value: type.id,
- label: type.name
- })),
- multiple: true
- }
- },
- customParser: (f) => {
- const filter = [];
-
- if (f.operator === OPERATORS.HAS.value) {
- const value = Array.isArray(f.value) ? f.value.join("||") : f.value;
- filter.push(`has_media_upload_with_type==${value}`);
- } else {
- const value = Array.isArray(f.value) ? f.value.join("&&") : f.value;
- filter.push(`has_not_media_upload_with_type==${value}`);
- }
-
- return filter;
- }
- },
- {
- key: "review_status",
- label: "Review Status",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: reviewStatusOptions,
- multiple: true
- }
- }
- },
- {
- key: "submission_source",
- label: "Submitter Status",
- operators: [OPERATORS.IS],
- values: {
- type: "select",
- props: {
- options: submissionSourceOptions
- }
- }
- }
-];
-
const SummitEventListPage = ({
events,
currentSummit,
@@ -932,7 +66,6 @@ const SummitEventListPage = ({
term,
currentPage,
perPage,
- lastPage,
order,
orderDir,
totalEvents,
@@ -975,7 +108,7 @@ const SummitEventListPage = ({
orderDir: od
} = mergedParams;
- getEvents(t, p, pp, o, od, parsedFilter, extraColumns);
+ getEvents(t, p, pp, o, od, parsedFilter, selectedColumns);
};
useEffect(() => {
@@ -1010,8 +143,8 @@ const SummitEventListPage = ({
setShowImportFromMUXModal(true);
};
- const handleEdit = (eventId) => {
- history.push(`/app/summits/${currentSummit.id}/events/${eventId}`);
+ const handleEdit = (row) => {
+ history.push(`/app/summits/${currentSummit.id}/events/${row.id}`);
};
const handleExport = (ev) => {
@@ -1023,23 +156,12 @@ const SummitEventListPage = ({
_getEvents({ page });
};
- const handleSort = (index, key, dir) => {
- let translatedKey = key;
- switch (key) {
- case "name":
- translatedKey = "last_name";
- break;
- case "submitter_company":
- translatedKey = "created_by_company";
- break;
- case "progress_flags":
- translatedKey = "actions";
- break;
- default:
- break;
- }
+ const handlePerPageChange = (newPerPage) => {
+ _getEvents({ perPage: newPerPage });
+ };
- _getEvents({ order: translatedKey, orderDir: dir });
+ const handleSort = (index, key, dir) => {
+ _getEvents({ order: toApiSortKey(key), orderDir: dir });
};
const handleSearch = (newTerm) => {
@@ -1050,19 +172,17 @@ const SummitEventListPage = ({
history.push(`/app/summits/${currentSummit.id}/events/new`);
};
- const handleDeleteEvent = (eventId) => {
- const event = events.find((e) => e.id === eventId);
-
+ const handleDeleteEvent = (row) => {
Swal.fire({
title: T.translate("general.are_you_sure"),
- text: `${T.translate("event_list.delete_event_warning")} ${event.title}`,
+ text: `${T.translate("event_list.delete_event_warning")} ${row.title}`,
type: "warning",
showCancelButton: true,
confirmButtonColor: "#DD6B55",
confirmButtonText: T.translate("general.yes_delete")
}).then((result) => {
if (result.value) {
- deleteEvent(eventId);
+ deleteEvent(row.id);
}
});
};
@@ -1086,35 +206,58 @@ const SummitEventListPage = ({
saveFilterCriteria(filterToSave);
};
+ const optionalColumns = getOptionalColumns(
+ currentSummit.selection_plans,
+ currentSummit.tracks,
+ currentSummit.event_types,
+ currentSummit.id
+ );
+
+ const columnDDLOptions = [
+ ...optionalColumns.map((oc) => ({ value: oc.columnKey, label: oc.label })),
+ {
+ value: "all_companies",
+ label: T.translate("event_list.all_companies")
+ },
+ {
+ value: "all",
+ label: T.translate("general.all")
+ }
+ ];
+
const handleColumnsChange = (ev) => {
- const { value } = ev.target;
- let newColumns = value;
- const allCompanies = ["submitter_company", "speaker_company", "sponsor"];
- const mediaUploadsSelected = newColumns.includes("media_uploads");
+ const { value: newColumns } = ev.target;
- newColumns = newColumns.filter((col) => col !== "media_uploads_display");
- if (mediaUploadsSelected) newColumns.push("media_uploads_display");
+ if (newColumns.includes("all")) {
+ setSelectedColumns(
+ columnDDLOptions.map((opt) => opt.value).filter((v) => v !== "all")
+ );
+ return;
+ }
+ const allCompanies = ["submitter_company", "speaker_company", "sponsor"];
+ const hadAllCompanies = selectedColumns.includes("all_companies");
const hasAllCompanies = newColumns.includes("all_companies");
- const selectedCompanies = selectedColumns.filter((c) =>
- allCompanies.includes(c)
- ).length;
- const newCompanies = newColumns.filter((c) =>
- allCompanies.includes(c)
- ).length;
- if (selectedColumns.includes("all_companies") && !hasAllCompanies) {
- newColumns = newColumns.filter((e) => !allCompanies.includes(e));
- } else if (hasAllCompanies) {
- if (newCompanies === 0) {
- newColumns = [...selectedColumns, ...allCompanies, "all_companies"];
- } else if (newCompanies === selectedCompanies) {
- newColumns = [
- ...new Set([...newColumns, ...allCompanies, "all_companies"])
- ];
- } else if (newCompanies < selectedCompanies) {
- newColumns = newColumns.filter((c) => c !== "all_companies");
- }
+ if (hadAllCompanies && !hasAllCompanies) {
+ setSelectedColumns(newColumns.filter((c) => !allCompanies.includes(c)));
+ return;
+ }
+
+ if (hasAllCompanies) {
+ const selectedCompanies = allCompanies.filter((c) =>
+ selectedColumns.includes(c)
+ ).length;
+ const newCompanies = allCompanies.filter((c) =>
+ newColumns.includes(c)
+ ).length;
+
+ setSelectedColumns(
+ newCompanies < selectedCompanies
+ ? newColumns.filter((c) => c !== "all_companies")
+ : [...new Set([...newColumns, ...allCompanies])]
+ );
+ return;
}
setSelectedColumns(newColumns);
@@ -1130,28 +273,13 @@ const SummitEventListPage = ({
});
};
- const translateSortKey = (key) => {
- switch (key) {
- case "last_name":
- return "name";
- case "created_by_company":
- return "submitter_company";
- case "actions":
- return "progress_flags";
- default:
- break;
- }
-
- return key;
- };
-
const eventTypeOptions = buildNameIdDDL(currentSummit.event_types);
- let columns = [
- { columnKey: "id", value: T.translate("general.id"), sortable: true },
+ const fixedColumns = [
+ { columnKey: "id", label: T.translate("general.id"), sortable: true },
{
columnKey: "type",
- value: T.translate("event_list.type"),
+ label: T.translate("event_list.type"),
sortable: true,
// eslint-disable-next-line react/no-unstable-nested-components
editableField: (extraProps) => (
@@ -1177,19 +305,24 @@ const SummitEventListPage = ({
},
{
columnKey: "title",
- value: T.translate("event_list.title"),
+ label: T.translate("event_list.title"),
sortable: true,
- editableField: true
+ editableField: true,
+ placeholder: T.translate("bulk_actions_page.placeholders.event_title")
},
{
columnKey: "selection_status",
- value: T.translate("event_list.selection_status"),
- sortable: true
+ label: T.translate("event_list.selection_status"),
+ sortable: true,
+ render: (status, row) =>
+ status === "unaccepted" && row.is_published === true
+ ? "accepted"
+ : status
}
];
const tableOptions = {
- sortCol: translateSortKey(order),
+ sortCol: toUiSortKey(order),
sortDir: orderDir,
className: "summit-event-list-table",
actions: {
@@ -1198,120 +331,16 @@ const SummitEventListPage = ({
}
};
- const columnOptions = [
- {
- value: "event_type_capacity",
- label: T.translate("event_list.event_type_capacity")
- },
- { value: "speakers", label: T.translate("event_list.speakers") },
- {
- value: "all_companies",
- label: T.translate("event_list.all_companies")
- },
- {
- value: "created_by_fullname",
- label: T.translate("event_list.created_by")
- },
- { value: "duration", label: T.translate("event_list.duration") },
- { value: "end_date", label: T.translate("event_list.end_date") },
- { value: "published_date", label: T.translate("event_list.published") },
- {
- value: "speaker_company",
- label: T.translate("event_list.speaker_company")
- },
- {
- value: "speakers_count",
- label: T.translate("event_list.speakers_count")
- },
- { value: "sponsor", label: T.translate("event_list.sponsor") },
- {
- value: "selection_plan",
- label: T.translate("event_list.selection_plan")
- },
- { value: "location", label: T.translate("event_list.location") },
- { value: "level", label: T.translate("event_list.level") },
- { value: "tags", label: T.translate("event_list.tags") },
- {
- value: "streaming_url",
- label: T.translate("event_list.streaming_url")
- },
- { value: "meeting_url", label: T.translate("event_list.meeting_url") },
- {
- value: "etherpad_link",
- label: T.translate("event_list.etherpad_link")
- },
- {
- value: "streaming_type",
- label: T.translate("event_list.streaming_type")
- },
- { value: "start_date", label: T.translate("event_list.start_date") },
- {
- value: "submitter_company",
- label: T.translate("event_list.submitter_company")
- },
- { value: "track", label: T.translate("event_list.track") },
- { value: "status", label: T.translate("event_list.submission_status") },
- {
- value: "submission_source",
- label: T.translate("event_list.submission_source")
- },
- {
- value: "progress_flags",
- label: T.translate("event_list.progress_flags")
- },
- {
- value: "media_uploads",
- label: T.translate("event_list.media_uploads")
- },
- {
- value: "review_status",
- label: T.translate("event_list.review_status")
- },
- { value: "created", label: T.translate("event_list.created") },
- { value: "modified", label: T.translate("event_list.modified") },
- {
- value: "allow_feedback",
- label: T.translate("event_list.allow_feedback")
- },
- {
- value: "to_record",
- label: T.translate("event_list.to_record")
- }
- ];
-
- const showColumns = fieldNames(
- currentSummit.selection_plans,
- currentSummit.tracks,
- currentSummit.event_types,
- currentSummit.id
- )
- .filter(
- (f) =>
- selectedColumns.includes(f.columnKey) &&
- !defaultColumns.includes(f.columnKey)
- )
- .map((f2) => {
- let c = {
- columnKey: f2.columnKey,
- value: T.translate(`event_list.${f2.value}`),
- sortable: f2.sortable,
- editable: !!editableColumns.includes(f2.editable),
- customStyle: f2.customStyle
- };
- // optional fields
- if (f2?.title) c = { ...c, title: f2.title };
-
- if (f2?.render) c = { ...c, render: f2.render };
-
- if (f2?.editableField) c = { ...c, editableField: f2.editableField };
-
- return c;
- });
+ const selectedOptionalColumns = optionalColumns.filter((c) =>
+ selectedColumns.includes(c.columnKey)
+ );
- columns = [...columns, ...showColumns];
+ const tableColumns = [...fixedColumns, ...selectedOptionalColumns];
if (!currentSummit.id) return ;
+ const tableData = events.map((e) => formatEventData(e, currentSummit));
+
return (
@@ -1393,7 +422,7 @@ const SummitEventListPage = ({
placeholder={T.translate("event_list.placeholders.select_fields")}
value={selectedColumns}
onChange={handleColumnsChange}
- options={handleDDLSortByLabel(columnOptions)}
+ options={handleDDLSortByLabel(columnDDLOptions)}
isClearable
isMulti
/>
@@ -1403,32 +432,18 @@ const SummitEventListPage = ({
{events.length === 0 &&
{T.translate("event_list.no_events")}
}
{events.length > 0 && (
-
)}
diff --git a/src/reducers/events/event-list-reducer.js b/src/reducers/events/event-list-reducer.js
index d1ce06753..7b90b1f7a 100644
--- a/src/reducers/events/event-list-reducer.js
+++ b/src/reducers/events/event-list-reducer.js
@@ -27,7 +27,7 @@ import { SET_CURRENT_SUMMIT } from "../../actions/summit-actions";
momentDurationFormatSetup(moment);
const DEFAULT_STATE = {
- events: {},
+ events: [],
term: null,
order: "id",
orderDir: 1,
diff --git a/src/utils/__tests__/summitUtils.test.js b/src/utils/__tests__/summitUtils.test.js
deleted file mode 100644
index 39c6f894f..000000000
--- a/src/utils/__tests__/summitUtils.test.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import { formatEventData } from "../summitUtils";
-
-describe("summitUtils.formatEventData", () => {
- const summit = {
- time_zone: { name: "UTC" },
- time_zone_id: "UTC"
- };
-
- test("returns speakers_count based on speakers length when type.use_speakers is true", () => {
- const event = {
- id: 10,
- summit_id: 1,
- title: "Test Event",
- is_published: false,
- type: {
- use_speakers: true,
- allows_location: false,
- allows_attendee_vote: false,
- allows_publishing_dates: false
- },
- speakers: [
- { first_name: "Jane", last_name: "Doe", company: "ACME" },
- { first_name: "John", last_name: "Smith", company: "Globex" }
- ],
- tags: [],
- sponsors: []
- };
-
- const result = formatEventData(event, summit);
-
- expect(result.speakers_count).toBe(2);
- });
-
- test("returns \"0\" when type.use_speakers is true and speakers list is empty", () => {
- const event = {
- id: 12,
- summit_id: 1,
- title: "No Speakers Yet",
- is_published: false,
- type: {
- use_speakers: true,
- allows_location: false,
- allows_attendee_vote: false,
- allows_publishing_dates: false
- },
- speakers: [],
- tags: [],
- sponsors: []
- };
-
- const result = formatEventData(event, summit);
-
- expect(result.speakers_count).toBe("0");
- });
-
- test("returns N/A and does not throw when event type is missing", () => {
- const event = {
- id: 11,
- summit_id: 1,
- title: "Type-less Event",
- is_published: false,
- speakers: [],
- tags: [],
- sponsors: []
- };
-
- let result;
- expect(() => {
- result = formatEventData(event, summit);
- }).not.toThrow();
- expect(result.speakers_count).toBe("N/A");
- });
-});
diff --git a/src/utils/methods.js b/src/utils/methods.js
index dd426a3dd..68aec70fa 100644
--- a/src/utils/methods.js
+++ b/src/utils/methods.js
@@ -634,5 +634,10 @@ export const formatDate = (
.format(format);
};
+export const formatDuration = (duration) => {
+ const d = moment.duration(duration, "seconds");
+ return d.format("mm:ss") !== "00" ? d.format("mm:ss") : "TBD";
+};
+
export const getFileUploadAllowedExtensions = () =>
window.FILE_UPLOAD_ALLOWED_EXTENSIONS?.split(",").filter(Boolean) ?? [];
diff --git a/src/utils/summitUtils.js b/src/utils/summitUtils.js
deleted file mode 100644
index 525446f23..000000000
--- a/src/utils/summitUtils.js
+++ /dev/null
@@ -1,163 +0,0 @@
-import moment from "moment-timezone";
-import { MILLISECONDS_IN_SECOND } from "./constants";
-
-// Default columns to show
-export const defaultColumns = ["id", "event_type", "title", "selection_status"];
-
-export const editableColumns = [
- "event_type",
- "title",
- "speakers",
- "track",
- "selection_plan",
- "streaming_url",
- "meeting_url",
- "etherpad_link"
-];
-
-const formatDuration = (duration) => {
- const d = moment.duration(duration, "seconds");
- return d.format("mm:ss") !== "00" ? d.format("mm:ss") : "TBD";
-};
-
-export const formatEventData = (e, summit) => {
- const published_date = e.is_published
- ? moment(e.published_date * MILLISECONDS_IN_SECOND)
- .tz(summit.time_zone.name)
- .format("MMMM Do YYYY, h:mm a")
- : "No";
-
- let speakers_companies =
- Array.isArray(e.speakers) && e.speakers.length > 0
- ? e.speakers?.map((e) => e.company)
- : [];
- speakers_companies =
- speakers_companies.length > 0
- ? speakers_companies.filter(
- (item, index) =>
- item !== "" && speakers_companies.indexOf(item) === index
- )
- : [];
-
- const event_type_capacity = [];
-
- if (e.type?.allows_location) event_type_capacity.push("Allows Location");
- if (e.type?.allows_attendee_vote)
- event_type_capacity.push("Allows Attendee Vote");
- if (e.type?.allows_publishing_dates)
- event_type_capacity.push("Allows Publishing Dates");
-
- let speakers_count;
-
- if (e.type?.use_speakers) {
- if (e.speakers && e.speakers.length > 0) {
- speakers_count = e.speakers.length;
- } else {
- speakers_count = "0";
- }
- } else {
- speakers_count = "N/A";
- }
-
- return {
- id: e.id,
- type: e.type,
- summit_id: e.summit_id,
- title: e.title,
- status: e.status ?? "Not Submitted",
- selection_status:
- e.selection_status === "unaccepted" && e.is_published === true
- ? "accepted"
- : e.selection_status,
- published_date,
- created_by_fullname: e.hasOwnProperty("created_by")
- ? `${e.created_by.first_name} ${e.created_by.last_name} (${e.created_by.email})`
- : "TBD",
- submitter_company: e.hasOwnProperty("created_by")
- ? e.created_by.company
- : "N/A",
- speakers:
- Array.isArray(e.speakers) && e.speakers.length > 0
- ? e.speakers.map((s) => `${s.first_name} ${s.last_name}`).join(", ")
- : "N/A",
- speaker_company:
- speakers_companies.length > 0
- ? speakers_companies.reduce(
- (accumulator, company) =>
- accumulator + (accumulator !== "" ? ", " : "") + company,
- ""
- )
- : "N/A",
- duration:
- e.type?.allows_publishing_dates && e.duration
- ? formatDuration(e?.duration)
- : "N/A",
- speakers_count,
- event_type_capacity: event_type_capacity.reduce(
- (accumulator, capacity) =>
- accumulator + (accumulator !== "" ? ", " : "") + capacity,
- ""
- ),
- track: e?.track?.name ? e?.track?.name : "TBD",
- level: e.level ? e.level : "N/A",
- tags:
- Array.isArray(e.tags) && e.tags.length > 0
- ? e.tags.reduce(
- (accumulator, t) =>
- accumulator + (accumulator !== "" ? ", " : "") + t.tag,
- ""
- )
- : "N/A",
- selection_plan: e.selection_plan ? e.selection_plan : "N/A",
- location: e.location?.name ? e.location?.name : "N/A",
- streaming_url: e.streaming_url ? e.streaming_url : "N/A",
- meeting_url: e.meeting_url ? e.meeting_url : "N/A",
- etherpad_link: e.etherpad_link ? e.etherpad_link : "N/A",
- streaming_type: e.streaming_type ? e.streaming_type : "N/A",
- start_date: e.start_date
- ? moment(e.start_date * MILLISECONDS_IN_SECOND)
- .tz(summit.time_zone.name)
- .format("MMMM Do YYYY, h:mm a")
- : "TBD",
- end_date: e.end_date
- ? moment(e.end_date * MILLISECONDS_IN_SECOND)
- .tz(summit.time_zone.name)
- .format("MMMM Do YYYY, h:mm a")
- : "TBD",
- sponsor:
- Array.isArray(e.sponsors) && e.sponsors.length > 0
- ? e.sponsors.map((s) => s.name).join(", ")
- : "N/A",
- media_uploads:
- Array.isArray(e.media_uploads) && e.media_uploads?.length > 0
- ? e?.media_uploads?.map((m) => ({
- ...m,
- created: moment(m.created * MILLISECONDS_IN_SECOND)
- .tz(summit.time_zone.name)
- .format("MMMM Do YYYY, h:mm a")
- }))
- : "N/A",
- created: e.created
- ? moment(e.created * MILLISECONDS_IN_SECOND)
- .tz(summit.time_zone_id)
- .format("MMMM Do YYYY, h:mm a")
- : "TBD",
- modified: e.last_edited
- ? moment(e.last_edited * MILLISECONDS_IN_SECOND)
- .tz(summit.time_zone_id)
- .format("MMMM Do YYYY, h:mm a")
- : "TBD",
- progress_flags: e?.actions
- ?.map((a) => `${a.type.label} (${a.is_completed ? "ON" : "OFF"})`)
- .join(", "),
- submission_source: e.submission_source ? e.submission_source : "N/A",
- review_status: e.review_status ?? "N/A"
- };
-};
-
-export const getIdValue = (prop) => {
- if (prop && typeof prop === "number") {
- return prop;
- }
- return prop?.id;
-};
diff --git a/yarn.lock b/yarn.lock
index 43300d058..827316200 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9056,10 +9056,10 @@ open@^10.0.3:
is-inside-container "^1.0.0"
wsl-utils "^0.1.0"
-openstack-uicore-foundation@5.0.36-beta.1:
- version "5.0.36-beta.1"
- resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.36-beta.1.tgz#2982cb73482ce662b0cd32c01eb4d4f88bbbf447"
- integrity sha512-g0bXh7z/vlWOlB/vSWN4R3SoLUAJgyU01WCf6+wbBAITkI9pJ/7tWStzAf6iZPyfu5r4q23ySyl6m7Z9Wy5PTQ==
+openstack-uicore-foundation@5.0.36-beta.2:
+ version "5.0.36-beta.2"
+ resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.36-beta.2.tgz#6d175e10fb24b4902e7d4ec88594b80f6e6eb64b"
+ integrity sha512-JyzM4mss5dAIlOsctwrKo3UxlLbu4PoXqaK+ORRwWZ4U8sszjZgY9IQ1WZbqGi/WYcE1m/DBM4sanoB9SdIXvg==
dependencies:
use-sync-external-store "^1.6.0"