diff --git a/package.json b/package.json
index ecdb0dd69..28f4a83ce 100644
--- a/package.json
+++ b/package.json
@@ -93,7 +93,7 @@
"moment-duration-format": "^2.3.2",
"moment-timezone": "^0.5.33",
"mui-color-input": "^9.0.0",
- "openstack-uicore-foundation": "5.0.38",
+ "openstack-uicore-foundation": "5.0.39",
"p-limit": "^6.1.0",
"path-browserify": "^1.0.1",
"postcss-loader": "^6.2.1",
diff --git a/src/actions/media-upload-actions.js b/src/actions/media-upload-actions.js
index 648398b3b..074867e16 100644
--- a/src/actions/media-upload-actions.js
+++ b/src/actions/media-upload-actions.js
@@ -20,15 +20,13 @@ import {
createAction,
stopLoading,
startLoading,
- showMessage,
- showSuccessMessage,
- authErrorHandler,
+ snackbarErrorHandler,
+ snackbarSuccessHandler,
escapeFilterValue,
fetchResponseHandler,
fetchErrorHandler
} from "openstack-uicore-foundation/lib/utils/actions";
-import debounce from "lodash/debounce"
-import history from "../history";
+import debounce from "lodash/debounce";
import { getAccessTokenSafely } from "../utils/methods";
import { DEBOUNCE_WAIT, DEFAULT_PER_PAGE } from "../utils/constants";
@@ -89,9 +87,9 @@ export const getMediaUploads =
createAction(REQUEST_MEDIA_UPLOADS),
createAction(RECEIVE_MEDIA_UPLOADS),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/media-upload-types`,
- authErrorHandler,
- { order, orderDir, term }
- )(params)(dispatch).then(() => {
+ snackbarErrorHandler,
+ { order, orderDir, term, perPage }
+ )(params)(dispatch).finally(() => {
dispatch(stopLoading());
});
};
@@ -111,94 +109,89 @@ export const getMediaUpload = (mediaUploadId) => async (dispatch, getState) => {
null,
createAction(RECEIVE_MEDIA_UPLOAD),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/media-upload-types/${mediaUploadId}`,
- authErrorHandler
- )(params)(dispatch).then(() => {
+ snackbarErrorHandler
+ )(params)(dispatch).finally(() => {
dispatch(stopLoading());
});
};
-export const queryMediaUploads = debounce(
- async (summitId, input, callback) => {
- const accessToken = await getAccessTokenSafely();
- const apiUrl = URI(
- `${window.API_BASE_URL}/api/v1/summits/${summitId}/media-upload-types`
- );
-
- apiUrl.addQuery("access_token", accessToken);
- apiUrl.addQuery("order", "name");
- apiUrl.addQuery("expand", "type");
- apiUrl.addQuery("per_page", DEFAULT_PER_PAGE);
-
- if (input) {
- input = escapeFilterValue(input);
- apiUrl.addQuery("filter[]", `name=@${input}`);
- }
+export const queryMediaUploads = debounce(async (summitId, input, callback) => {
+ const accessToken = await getAccessTokenSafely();
+ const apiUrl = URI(
+ `${window.API_BASE_URL}/api/v1/summits/${summitId}/media-upload-types`
+ );
+
+ apiUrl.addQuery("access_token", accessToken);
+ apiUrl.addQuery("order", "name");
+ apiUrl.addQuery("expand", "type");
+ apiUrl.addQuery("per_page", DEFAULT_PER_PAGE);
+
+ if (input) {
+ input = escapeFilterValue(input);
+ apiUrl.addQuery("filter[]", `name=@${input}`);
+ }
- fetch(apiUrl.toString())
- .then(fetchResponseHandler)
- .then((json) => {
- const options = [...json.data];
- callback(options);
- })
- .catch(fetchErrorHandler);
- },
- DEBOUNCE_WAIT
-);
+ fetch(apiUrl.toString())
+ .then(fetchResponseHandler)
+ .then((json) => {
+ const options = [...json.data];
+ callback(options);
+ })
+ .catch(fetchErrorHandler);
+}, DEBOUNCE_WAIT);
export const resetMediaUploadForm = () => (dispatch) => {
dispatch(createAction(RESET_MEDIA_UPLOAD_FORM)({}));
};
-export const saveMediaUpload =
- (entity, noAlert = false) =>
- async (dispatch, getState) => {
- const { currentSummitState } = getState();
- const accessToken = await getAccessTokenSafely();
- const { currentSummit } = currentSummitState;
+export const saveMediaUpload = (entity) => async (dispatch, getState) => {
+ const { currentSummitState } = getState();
+ const accessToken = await getAccessTokenSafely();
+ const { currentSummit } = currentSummitState;
- dispatch(startLoading());
+ dispatch(startLoading());
- const normalizedEntity = normalizeEntity(entity);
- const params = { access_token: accessToken };
+ const normalizedEntity = normalizeEntity(entity);
+ const params = { access_token: accessToken };
- if (entity.id) {
- putRequest(
- createAction(UPDATE_MEDIA_UPLOAD),
- createAction(MEDIA_UPLOAD_UPDATED),
- `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/media-upload-types/${entity.id}`,
- normalizedEntity,
- authErrorHandler,
- entity
- )(params)(dispatch).then(() => {
- if (!noAlert)
- dispatch(showSuccessMessage(T.translate("media_upload.saved")));
- else dispatch(stopLoading());
- });
- } else {
- const successMessage = {
- title: T.translate("general.done"),
- html: T.translate("media_upload.created"),
- type: "success"
- };
-
- postRequest(
- createAction(UPDATE_MEDIA_UPLOAD),
- createAction(MEDIA_UPLOAD_ADDED),
- `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/media-upload-types`,
- normalizedEntity,
- authErrorHandler,
- entity
- )(params)(dispatch).then((payload) => {
+ if (entity.id) {
+ return putRequest(
+ createAction(UPDATE_MEDIA_UPLOAD),
+ createAction(MEDIA_UPLOAD_UPDATED),
+ `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/media-upload-types/${entity.id}`,
+ normalizedEntity,
+ snackbarErrorHandler,
+ entity
+ )(params)(dispatch)
+ .then(() => {
dispatch(
- showMessage(successMessage, () => {
- history.push(
- `/app/summits/${currentSummit.id}/media-uploads/${payload.response.id}`
- );
+ snackbarSuccessHandler({
+ title: T.translate("general.success"),
+ html: T.translate("media_upload.saved")
})
);
- });
- }
- };
+ })
+ .finally(() => dispatch(stopLoading()));
+ }
+
+ return postRequest(
+ createAction(UPDATE_MEDIA_UPLOAD),
+ createAction(MEDIA_UPLOAD_ADDED),
+ `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/media-upload-types`,
+ normalizedEntity,
+ snackbarErrorHandler,
+ entity
+ )(params)(dispatch)
+ .then(() => {
+ dispatch(
+ snackbarSuccessHandler({
+ title: T.translate("general.success"),
+ html: T.translate("media_upload.created")
+ })
+ );
+ })
+ .finally(() => dispatch(stopLoading()));
+};
export const linkToPresentationType =
(mediaUpload, presentationTypeId) => async (dispatch, getState) => {
@@ -215,8 +208,8 @@ export const linkToPresentationType =
createAction(MEDIA_UPLOAD_LINKED)({ mediaUpload }),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/media-upload-types/${mediaUpload.id}/presentation-types/${presentationTypeId}`,
null,
- authErrorHandler
- )(params)(dispatch).then(() => {
+ snackbarErrorHandler
+ )(params)(dispatch).finally(() => {
dispatch(stopLoading());
});
};
@@ -236,8 +229,8 @@ export const unlinkFromPresentationType =
createAction(MEDIA_UPLOAD_UNLINKED)({ mediaUploadId }),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/media-upload-types/${mediaUploadId}/presentation-types/${presentationTypeId}`,
null,
- authErrorHandler
- )(params)(dispatch).then(() => {
+ snackbarErrorHandler
+ )(params)(dispatch).finally(() => {
dispatch(stopLoading());
});
};
@@ -257,10 +250,8 @@ export const deleteMediaUpload =
createAction(MEDIA_UPLOAD_DELETED)({ mediaUploadId }),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/media-upload-types/${mediaUploadId}`,
null,
- authErrorHandler
- )(params)(dispatch).then(() => {
- dispatch(stopLoading());
- });
+ snackbarErrorHandler
+ )(params)(dispatch);
};
export const copyMediaUploads = (summitId) => async (dispatch, getState) => {
@@ -272,16 +263,23 @@ export const copyMediaUploads = (summitId) => async (dispatch, getState) => {
const params = { access_token: accessToken };
- postRequest(
+ return postRequest(
null,
createAction(MEDIA_UPLOADS_COPIED),
`${window.API_BASE_URL}/api/v1/summits/${summitId}/media-upload-types/all/clone/${currentSummit.id}`,
null,
- authErrorHandler
- )(params)(dispatch).then(() => {
- dispatch(stopLoading());
- dispatch(getMediaUploads());
- });
+ snackbarErrorHandler
+ )(params)(dispatch)
+ .then(() => {
+ dispatch(
+ snackbarSuccessHandler({
+ title: T.translate("general.success"),
+ html: T.translate("media_upload.media_uploads_copied")
+ })
+ );
+ dispatch(getMediaUploads());
+ })
+ .finally(() => dispatch(stopLoading()));
};
const normalizeEntity = (entity) => {
diff --git a/src/components/forms/media-upload-form.js b/src/components/forms/media-upload-form.js
deleted file mode 100644
index f73670a6c..000000000
--- a/src/components/forms/media-upload-form.js
+++ /dev/null
@@ -1,312 +0,0 @@
-/**
- * Copyright 2019 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 "awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css";
-import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown"
-import Input from "openstack-uicore-foundation/lib/components/inputs/text-input";
-import TextEditorV3 from "openstack-uicore-foundation/lib/components/inputs/editor-input-v3";
-import { isEmpty, scrollToError, shallowEqual } from "../../utils/methods";
-
-class MediaUploadForm extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- entity: { ...props.entity },
- errors: props.errors
- };
-
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- }
-
- componentDidUpdate(prevProps) {
- const state = {};
- scrollToError(this.props.errors);
-
- if (!shallowEqual(prevProps.entity, this.props.entity)) {
- state.entity = { ...this.props.entity };
- state.errors = {};
- }
-
- if (!shallowEqual(prevProps.errors, this.props.errors)) {
- state.errors = { ...this.props.errors };
- }
-
- if (!isEmpty(state)) {
- this.setState({ ...this.state, ...state });
- }
- }
-
- handleChange(ev) {
- const entity = { ...this.state.entity };
- const errors = { ...this.state.errors };
- let { value, id } = ev.target;
-
- if (ev.target.type === "checkbox") {
- value = ev.target.checked;
- }
-
- errors[id] = "";
- entity[id] = value;
- this.setState({ entity, errors });
- }
-
- handleSubmit(ev) {
- ev.preventDefault();
-
- this.props.onSubmit(this.state.entity);
- }
-
- hasErrors(field) {
- const { errors } = this.state;
- if (field in errors) {
- return errors[field];
- }
-
- return "";
- }
-
- render() {
- const { entity } = this.state;
- const { currentSummit, mediaFileTypes } = this.props;
-
- const private_storage_ddl = [
- { value: "None", label: "None" },
- { value: "DropBox", label: "DropBox" },
- { value: "Local", label: "Local" }
- ];
-
- const public_storage_ddl = [
- { value: "None", label: "None" },
- { value: "Local", label: "Local" }
- ];
-
- if (window.PUBLIC_STORAGES.includes("S3"))
- public_storage_ddl.push({ value: "S3", label: "S3" });
-
- if (window.PUBLIC_STORAGES.includes("SWIFT"))
- public_storage_ddl.push({ value: "Swift", label: "Swift" });
-
- const presentation_types_ddl = currentSummit.event_types
- .filter((t) => t.class_name === "PresentationType")
- .map((t) => ({ value: t.id, label: t.name }));
-
- const mediaFileTypesDDL = mediaFileTypes.map((mft) => ({
- value: mft.id,
- label: mft.name
- }));
-
- return (
-
- );
- }
-}
-
-export default MediaUploadForm;
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 4861975f0..0b946a520 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -3524,18 +3524,19 @@
"private_storage_type": "Private Storage Type",
"presentation_types": "Presentation Types",
"copy_media_uploads": "Copy Media Uploads",
+ "media_uploads_copied": "Media Upload Types copied successfully.",
"delete_warning": "Are you sure you want to delete media media type ",
"saved": "Media Upload Type saved successfully.",
"created": "Media Upload Type created successfully.",
"temporary_links_on_public_storage": "Temporary Links",
"use_temporary_links_on_public_storage": "Use Temporary Links",
"temporary_links_public_storage_ttl_info": "Temporary Link Time To Live (TTL)",
+ "minutes": "minutes",
"placeholders": {
"search": "Search types by name",
"select_type": "Select File Type",
"select_private_storage": "Select Public Storage",
"select_public_storage": "Select Private Storage",
- "select_presentation_types": "Select Presentation Types",
"temporary_links_public_storage_ttl": "TTL"
}
},
diff --git a/src/layouts/media-upload-layout.js b/src/layouts/media-upload-layout.js
index c028a1dc7..839866405 100644
--- a/src/layouts/media-upload-layout.js
+++ b/src/layouts/media-upload-layout.js
@@ -9,53 +9,36 @@
* 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 { Switch, Route } from "react-router-dom";
+import PropTypes from "prop-types";
+import { Switch, Route, withRouter } from "react-router-dom";
import T from "i18n-react/dist/i18n-react";
-import { connect } from "react-redux";
import { Breadcrumb } from "react-breadcrumbs";
import Restrict from "../routes/restrict";
+
import MediaUploadListPage from "../pages/media_uploads/media-upload-list-page";
-import EditMediaUploadPage from "../pages/media_uploads/edit-media-upload-page";
import NoMatchPage from "../pages/no-match-page";
-class MediaUploadLayout extends React.Component {
- render() {
- const { match } = this.props;
+const MediaUploadLayout = ({ match }) => (
+
+
- return (
-
-
+
+
+
+
+
+);
-
-
-
-
-
-
-
- );
- }
-}
+MediaUploadLayout.propTypes = {
+ match: PropTypes.shape({ url: PropTypes.string }).isRequired
+};
-export default Restrict(connect(null, {})(MediaUploadLayout), "events");
+export default Restrict(withRouter(MediaUploadLayout), "events");
diff --git a/src/pages/media_uploads/__tests__/media-upload-list-page.test.js b/src/pages/media_uploads/__tests__/media-upload-list-page.test.js
new file mode 100644
index 000000000..fbd0b28d1
--- /dev/null
+++ b/src/pages/media_uploads/__tests__/media-upload-list-page.test.js
@@ -0,0 +1,252 @@
+import React from "react";
+import { act, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import "@testing-library/jest-dom";
+import flushPromises from "flush-promises";
+import { renderWithRedux } from "../../../utils/test-utils";
+import MediaUploadListPage from "../media-upload-list-page";
+import {
+ getMediaUploads,
+ getMediaUpload,
+ deleteMediaUpload,
+ copyMediaUploads,
+ resetMediaUploadForm,
+ saveMediaUpload
+} from "../../../actions/media-upload-actions";
+import { getAllMediaFileTypes } from "../../../actions/media-file-type-actions";
+
+jest.mock("../../../actions/media-upload-actions", () => ({
+ getMediaUploads: jest.fn(),
+ getMediaUpload: jest.fn(),
+ deleteMediaUpload: jest.fn(),
+ copyMediaUploads: jest.fn(),
+ resetMediaUploadForm: jest.fn(),
+ saveMediaUpload: jest.fn()
+}));
+
+jest.mock("../../../actions/media-file-type-actions", () => ({
+ getAllMediaFileTypes: jest.fn()
+}));
+
+jest.mock("openstack-uicore-foundation/lib/components/mui/table", () => ({
+ __esModule: true,
+ default: ({ onEdit, onDelete, onSort, onPageChange, onPerPageChange }) => (
+
+
+
+
+
+
+
+ )
+}));
+
+jest.mock(
+ "openstack-uicore-foundation/lib/components/mui/search-input",
+ () => ({
+ __esModule: true,
+ default: ({ onSearch }) => (
+
+ )
+ })
+);
+
+jest.mock("../../../components/summit-dropdown", () => ({
+ __esModule: true,
+ default: ({ onClick }) => (
+
+ )
+}));
+
+// The stub mirrors the real popup contract from the popup-dialog pattern:
+// it closes only when the promise returned by onSave resolves.
+jest.mock("../components/media-upload-dialog", () => ({
+ __esModule: true,
+ default: ({ onSave, onClose }) => (
+
+
+
+
+ )
+}));
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key) => key }
+}));
+
+const initialState = {
+ currentSummitState: {
+ currentSummit: { id: 42 }
+ },
+ mediaUploadListState: {
+ media_uploads: [{ id: 7, name: "Slides", description: "Slides upload" }],
+ totalMediaUploads: 1,
+ perPage: 10,
+ currentPage: 3,
+ term: "",
+ order: "id",
+ orderDir: 1
+ },
+ mediaUploadState: {
+ entity: { id: 0, name: "", description: "" },
+ errors: {},
+ media_file_types: []
+ }
+};
+
+describe("MediaUploadListPage", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ getMediaUploads.mockReturnValue(() => Promise.resolve());
+ getMediaUpload.mockReturnValue(() => Promise.resolve());
+ deleteMediaUpload.mockReturnValue(() => Promise.resolve());
+ saveMediaUpload.mockReturnValue(() => Promise.resolve());
+ copyMediaUploads.mockReturnValue(() => Promise.resolve());
+ resetMediaUploadForm.mockReturnValue({ type: "RESET_MEDIA_UPLOAD_FORM" });
+ getAllMediaFileTypes.mockReturnValue(() => Promise.resolve());
+ });
+
+ it("fetches media uploads and media file types on mount", () => {
+ renderWithRedux(, { initialState });
+
+ expect(getMediaUploads).toHaveBeenCalledTimes(1);
+ expect(getAllMediaFileTypes).toHaveBeenCalledTimes(1);
+ });
+
+ it("threads search, pagination and sort params to getMediaUploads", async () => {
+ renderWithRedux(, { initialState });
+
+ await userEvent.click(screen.getByRole("button", { name: "search-foo" }));
+ expect(getMediaUploads).toHaveBeenLastCalledWith("foo", 1, 10, "id", 1);
+
+ await userEvent.click(screen.getByRole("button", { name: "page-2" }));
+ expect(getMediaUploads).toHaveBeenLastCalledWith("", 2, 10, "id", 1);
+
+ await userEvent.click(screen.getByRole("button", { name: "per-page-20" }));
+ expect(getMediaUploads).toHaveBeenLastCalledWith("", 1, 20, "id", 1);
+
+ await userEvent.click(screen.getByRole("button", { name: "sort-name" }));
+ expect(getMediaUploads).toHaveBeenLastCalledWith("", 1, 10, "name", 0);
+ });
+
+ it("fetches the entity and opens the dialog when clicking edit", async () => {
+ renderWithRedux(, { initialState });
+
+ expect(screen.queryByTestId("media-upload-dialog")).not.toBeInTheDocument();
+
+ await act(async () => {
+ await userEvent.click(screen.getByRole("button", { name: "edit-row" }));
+ await flushPromises();
+ });
+
+ expect(getMediaUpload).toHaveBeenCalledWith(7);
+ expect(screen.getByTestId("media-upload-dialog")).toBeInTheDocument();
+ });
+
+ it("reloads the list after a successful save", async () => {
+ renderWithRedux(, { initialState });
+
+ await act(async () => {
+ await userEvent.click(screen.getByRole("button", { name: "edit-row" }));
+ await flushPromises();
+ });
+
+ await act(async () => {
+ await userEvent.click(screen.getByRole("button", { name: "popup-save" }));
+ await flushPromises();
+ });
+
+ expect(saveMediaUpload).toHaveBeenCalledWith({ id: 7 });
+ // Call 1: useEffect on mount; call 2: handleSave refresh
+ expect(getMediaUploads).toHaveBeenCalledTimes(2);
+ expect(getMediaUploads).toHaveBeenLastCalledWith("", 1, 10, "id", 1);
+ });
+
+ it("closes the dialog on save success even if the list refresh fails", async () => {
+ getMediaUploads
+ .mockReturnValueOnce(() => Promise.resolve()) // mount fetch
+ .mockReturnValue(() => Promise.reject(new Error("refresh failed")));
+
+ renderWithRedux(, { initialState });
+
+ await act(async () => {
+ await userEvent.click(screen.getByRole("button", { name: "edit-row" }));
+ await flushPromises();
+ });
+
+ await act(async () => {
+ await userEvent.click(screen.getByRole("button", { name: "popup-save" }));
+ await flushPromises();
+ });
+
+ expect(saveMediaUpload).toHaveBeenCalledTimes(1);
+ expect(screen.queryByTestId("media-upload-dialog")).not.toBeInTheDocument();
+ });
+
+ it("reloads the list after a successful delete", async () => {
+ renderWithRedux(, { initialState });
+
+ await act(async () => {
+ await userEvent.click(screen.getByRole("button", { name: "delete-row" }));
+ await flushPromises();
+ });
+
+ expect(deleteMediaUpload).toHaveBeenCalledWith(7);
+ // Call 1: useEffect on mount; call 2: handleDelete refresh
+ expect(getMediaUploads).toHaveBeenCalledTimes(2);
+ });
+
+ it("copies media uploads from the selected summit", async () => {
+ renderWithRedux(, { initialState });
+
+ await act(async () => {
+ await userEvent.click(
+ screen.getByRole("button", { name: "copy-media-uploads" })
+ );
+ await flushPromises();
+ });
+
+ expect(copyMediaUploads).toHaveBeenCalledWith(99);
+ });
+
+ it("resets the form when opening the add dialog and unmounts it on close", async () => {
+ renderWithRedux(, { initialState });
+
+ await userEvent.click(
+ screen.getByRole("button", { name: "media_upload.add" })
+ );
+
+ expect(resetMediaUploadForm).toHaveBeenCalledTimes(1);
+ expect(screen.getByTestId("media-upload-dialog")).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole("button", { name: "popup-close" }));
+
+ expect(screen.queryByTestId("media-upload-dialog")).not.toBeInTheDocument();
+ });
+});
diff --git a/src/pages/media_uploads/components/__tests__/media-upload-dialog.test.js b/src/pages/media_uploads/components/__tests__/media-upload-dialog.test.js
new file mode 100644
index 000000000..03e256dd9
--- /dev/null
+++ b/src/pages/media_uploads/components/__tests__/media-upload-dialog.test.js
@@ -0,0 +1,312 @@
+import React from "react";
+import {
+ render,
+ screen,
+ waitFor,
+ act,
+ fireEvent
+} from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import MediaUploadDialog from "../media-upload-dialog";
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ translate: jest.fn((key) => key)
+}));
+
+jest.mock(
+ "openstack-uicore-foundation/lib/components/mui/formik-inputs/textfield",
+ () =>
+ function MockTextField({ name }) {
+ // eslint-disable-next-line global-require
+ const { useField } = require("formik");
+ const [field, meta] = useField(name);
+ return (
+ <>
+
+ {meta.touched && meta.error && {meta.error}}
+ >
+ );
+ }
+);
+
+jest.mock(
+ "openstack-uicore-foundation/lib/components/mui/formik-inputs/file-size-field",
+ () =>
+ function MockFilesizeField({ name }) {
+ // eslint-disable-next-line global-require
+ const { useField } = require("formik");
+ const [field, meta] = useField(name);
+ return (
+ <>
+
+ {meta.touched && meta.error && {meta.error}}
+ >
+ );
+ }
+);
+
+jest.mock(
+ "../../../../components/mui/formik-inputs/mui-formik-select",
+ () =>
+ function MockSelect({ name, children }) {
+ return {children}
;
+ }
+);
+
+jest.mock(
+ "../../../../components/inputs/formik-text-editor",
+ () =>
+ function MockTextEditor({ name }) {
+ return ;
+ }
+);
+
+jest.mock("../../../../hooks/useScrollToError", () => jest.fn());
+
+const BASE_ENTITY = {
+ id: 0,
+ name: "",
+ description: "",
+ type_id: 0,
+ max_size: 0,
+ min_uploads_qty: 0,
+ max_uploads_qty: 0,
+ is_mandatory: false,
+ is_editable: true,
+ private_storage_type: "None",
+ public_storage_type: "None",
+ presentation_types: [],
+ use_temporary_links_on_public_storage: false,
+ temporary_links_public_storage_ttl: 0
+};
+
+const CURRENT_SUMMIT = {
+ id: 42,
+ event_types: [
+ { id: 1, name: "Talk", class_name: "PresentationType" },
+ { id: 2, name: "Panel", class_name: "PresentationType" },
+ { id: 3, name: "Keynote", class_name: "SummitEventType" }
+ ]
+};
+
+describe("MediaUploadDialog", () => {
+ let onSave;
+ let onClose;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ window.PUBLIC_STORAGES = [];
+ onSave = jest.fn(() => Promise.resolve());
+ onClose = jest.fn();
+ });
+
+ afterEach(() => {
+ delete window.PUBLIC_STORAGES;
+ });
+
+ const renderDialog = (
+ entity = BASE_ENTITY,
+ errors = {},
+ mediaFileTypes = []
+ ) =>
+ render(
+
+ );
+
+ it("renders the name and max_size fields by default", () => {
+ renderDialog();
+
+ expect(screen.getByTestId("textfield-name")).toBeInTheDocument();
+ expect(screen.getByTestId("filesize-max_size")).toBeInTheDocument();
+ });
+
+ it("only shows the TTL field once use_temporary_links_on_public_storage is checked", async () => {
+ const user = userEvent.setup();
+ renderDialog();
+
+ expect(
+ screen.queryByTestId("textfield-temporary_links_public_storage_ttl")
+ ).not.toBeInTheDocument();
+
+ await act(async () => {
+ await user.click(
+ screen.getByRole("checkbox", {
+ name: "media_upload.use_temporary_links_on_public_storage"
+ })
+ );
+ });
+
+ expect(
+ screen.getByTestId("textfield-temporary_links_public_storage_ttl")
+ ).toBeInTheDocument();
+ });
+
+ it("disables the save button while saving and re-enables after resolve", async () => {
+ let resolveSave;
+ onSave = jest.fn(
+ () =>
+ new Promise((res) => {
+ resolveSave = res;
+ })
+ );
+ const user = userEvent.setup();
+ renderDialog({ ...BASE_ENTITY, name: "Slides", max_size: 1024 });
+
+ const saveButton = screen.getByText("general.save").closest("button");
+ expect(saveButton).not.toBeDisabled();
+
+ await act(async () => {
+ await user.click(saveButton);
+ });
+
+ expect(saveButton).toBeDisabled();
+
+ await act(async () => {
+ resolveSave();
+ });
+
+ await waitFor(() => expect(saveButton).not.toBeDisabled());
+ });
+
+ it("keeps the dialog open and re-enables save when onSave rejects", async () => {
+ onSave = jest.fn(() => Promise.reject(new Error("server error")));
+ const user = userEvent.setup();
+ renderDialog({ ...BASE_ENTITY, name: "Slides", max_size: 1024 });
+
+ const saveButton = screen.getByText("general.save").closest("button");
+
+ await act(async () => {
+ await user.click(saveButton);
+ });
+
+ await waitFor(() => expect(saveButton).not.toBeDisabled());
+ expect(onClose).not.toHaveBeenCalled();
+ });
+
+ it("calls onClose when the close icon is clicked and not saving", async () => {
+ const user = userEvent.setup();
+ renderDialog();
+
+ await act(async () => {
+ await user.click(screen.getByLabelText("close"));
+ });
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+
+ describe("Yup validation", () => {
+ it("blocks submit and shows a required error when name is empty", async () => {
+ const user = userEvent.setup();
+ renderDialog({ ...BASE_ENTITY, name: "", max_size: 1024 });
+
+ await act(async () => {
+ await user.click(screen.getByText("general.save").closest("button"));
+ });
+
+ expect(
+ screen.getByTestId("textfield-name").nextSibling
+ ).toHaveTextContent("validation.required");
+ expect(onSave).not.toHaveBeenCalled();
+ });
+
+ it("submits 0 instead of NaN when max_size is cleared to empty", async () => {
+ const user = userEvent.setup();
+ renderDialog({ ...BASE_ENTITY, name: "Slides", max_size: 1024 });
+
+ const maxSizeInput = screen.getByTestId("filesize-max_size");
+ await act(async () => {
+ await user.clear(maxSizeInput);
+ await user.click(screen.getByText("general.save").closest("button"));
+ });
+
+ expect(onSave).toHaveBeenCalledWith(
+ expect.objectContaining({ max_size: 0 })
+ );
+ const [submittedValues] = onSave.mock.calls[0];
+ expect(Number.isNaN(submittedValues.max_size)).toBe(false);
+ });
+ });
+
+ it("renders an inline error for a field when the errors prop is populated", () => {
+ renderDialog(
+ { ...BASE_ENTITY, name: "Slides", max_size: 1024 },
+ { name: "Name already in use" }
+ );
+
+ expect(screen.getByText("Name already in use")).toBeInTheDocument();
+ });
+
+ it("clears a stale server-side error once the user edits the field", async () => {
+ renderDialog(
+ { ...BASE_ENTITY, name: "Slides", max_size: 1024 },
+ { name: "Name already in use" }
+ );
+
+ expect(screen.getByText("Name already in use")).toBeInTheDocument();
+
+ const nameInput = screen.getByTestId("textfield-name");
+ fireEvent.change(nameInput, { target: { value: "New Slides" } });
+
+ await waitFor(() =>
+ expect(screen.queryByText("Name already in use")).not.toBeInTheDocument()
+ );
+ });
+
+ describe("presentation_types", () => {
+ it("shows pre-selected presentation types as chips when editing an existing entity", () => {
+ renderDialog({
+ ...BASE_ENTITY,
+ id: 7,
+ name: "Slides",
+ max_size: 1024,
+ presentation_types: [1, 2]
+ });
+
+ expect(screen.getByText("Talk")).toBeInTheDocument();
+ expect(screen.getByText("Panel")).toBeInTheDocument();
+ expect(screen.queryByText("Keynote")).not.toBeInTheDocument();
+ });
+
+ it("adds the selected presentation type id and submits it on save", async () => {
+ const user = userEvent.setup();
+ renderDialog({ ...BASE_ENTITY, name: "Slides", max_size: 1024 });
+
+ const combobox = screen.getByRole("combobox", {
+ name: "media_upload.presentation_types"
+ });
+
+ await act(async () => {
+ await user.click(combobox);
+ });
+
+ await act(async () => {
+ await user.click(screen.getByText("Talk"));
+ });
+
+ await act(async () => {
+ await user.click(screen.getByText("general.save").closest("button"));
+ });
+
+ expect(onSave).toHaveBeenCalledWith(
+ expect.objectContaining({ presentation_types: [1] })
+ );
+ });
+ });
+});
diff --git a/src/pages/media_uploads/components/media-upload-dialog.js b/src/pages/media_uploads/components/media-upload-dialog.js
new file mode 100644
index 000000000..50f2163b0
--- /dev/null
+++ b/src/pages/media_uploads/components/media-upload-dialog.js
@@ -0,0 +1,393 @@
+/**
+ * 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, { useEffect, useState } 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 Autocomplete from "@mui/material/Autocomplete";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Dialog from "@mui/material/Dialog";
+import DialogActions from "@mui/material/DialogActions";
+import DialogContent from "@mui/material/DialogContent";
+import DialogTitle from "@mui/material/DialogTitle";
+import Divider from "@mui/material/Divider";
+import Grid2 from "@mui/material/Grid2";
+import IconButton from "@mui/material/IconButton";
+import InputAdornment from "@mui/material/InputAdornment";
+import InputLabel from "@mui/material/InputLabel";
+import MenuItem from "@mui/material/MenuItem";
+import TextField from "@mui/material/TextField";
+import CloseIcon from "@mui/icons-material/Close";
+import MuiFormikTextField from "openstack-uicore-foundation/lib/components/mui/formik-inputs/textfield";
+import MuiFormikCheckbox from "openstack-uicore-foundation/lib/components/mui/formik-inputs/checkbox";
+import MuiFormikFilesizeField from "openstack-uicore-foundation/lib/components/mui/formik-inputs/file-size-field";
+import useScrollToError from "../../../hooks/useScrollToError";
+import MuiFormikSelect from "../../../components/mui/formik-inputs/mui-formik-select";
+import FormikTextEditor from "../../../components/inputs/formik-text-editor";
+import { positiveNumberValidation } from "../../../utils/yup";
+
+const NUMERIC_FIELDS = [
+ "max_size",
+ "min_uploads_qty",
+ "max_uploads_qty",
+ "temporary_links_public_storage_ttl"
+];
+
+const PRIVATE_STORAGE_DDL = [
+ { value: "None", label: "None" },
+ { value: "DropBox", label: "DropBox" },
+ { value: "Local", label: "Local" }
+];
+
+const MediaUploadDialog = ({
+ currentSummit,
+ entity,
+ errors,
+ mediaFileTypes,
+ onClose,
+ onSave
+}) => {
+ const [isSaving, setIsSaving] = useState(false);
+
+ const publicStorageDdl = [
+ { value: "None", label: "None" },
+ { value: "Local", label: "Local" }
+ ];
+
+ if (window.PUBLIC_STORAGES.includes("S3"))
+ publicStorageDdl.push({ value: "S3", label: "S3" });
+
+ if (window.PUBLIC_STORAGES.includes("SWIFT"))
+ publicStorageDdl.push({ value: "Swift", label: "Swift" });
+
+ const presentationTypeOptions = currentSummit.event_types
+ .filter((t) => t.class_name === "PresentationType")
+ .map((t) => ({ id: t.id, name: t.name }));
+
+ const mediaFileTypesDdl = mediaFileTypes.map((mft) => ({
+ value: mft.id,
+ label: mft.name
+ }));
+
+ const handleSubmit = (values) => {
+ if (isSaving) return Promise.resolve();
+ const normalizedValues = { ...values };
+ NUMERIC_FIELDS.forEach((field) => {
+ const parsed = parseInt(values[field], 10);
+ // A cleared/non-numeric field must never serialize as NaN.
+ normalizedValues[field] = Number.isNaN(parsed) ? 0 : parsed;
+ });
+
+ setIsSaving(true);
+ return Promise.resolve(onSave(normalizedValues))
+ .then(() => onClose())
+ .catch(() => {
+ // keep dialog open on save error to preserve user input
+ })
+ .finally(() => setIsSaving(false));
+ };
+
+ const formik = useFormik({
+ initialValues: entity,
+ validationSchema: yup.object().shape({
+ name: yup.string().required(T.translate("validation.required")),
+ max_size: positiveNumberValidation(),
+ min_uploads_qty: positiveNumberValidation(),
+ max_uploads_qty: positiveNumberValidation(),
+ temporary_links_public_storage_ttl: positiveNumberValidation()
+ }),
+ onSubmit: handleSubmit
+ });
+
+ const { values, setFieldValue } = formik;
+
+ useScrollToError(formik, true);
+
+ useEffect(() => {
+ const errorFields = Object.keys(errors || {});
+ formik.setErrors(errorFields.length > 0 ? errors : {});
+ if (errorFields.length > 0) {
+ formik.setTouched(
+ errorFields.reduce((acc, field) => ({ ...acc, [field]: true }), {})
+ );
+ }
+ }, [errors]);
+
+ const handleClose = () => {
+ if (isSaving) return;
+ onClose();
+ };
+
+ const isEdit = Boolean(entity.id);
+ const title = `${T.translate(
+ isEdit ? "general.edit" : "general.add"
+ )} ${T.translate("media_upload.media_upload")}`;
+
+ return (
+
+ );
+};
+
+MediaUploadDialog.propTypes = {
+ currentSummit: PropTypes.shape({
+ event_types: PropTypes.arrayOf(PropTypes.shape({}))
+ }).isRequired,
+ entity: PropTypes.shape({
+ id: PropTypes.number,
+ presentation_types: PropTypes.arrayOf(PropTypes.number)
+ }).isRequired,
+ errors: PropTypes.shape({}),
+ mediaFileTypes: PropTypes.arrayOf(PropTypes.shape({})),
+ onClose: PropTypes.func.isRequired,
+ onSave: PropTypes.func.isRequired
+};
+
+MediaUploadDialog.defaultProps = {
+ errors: {},
+ mediaFileTypes: []
+};
+
+export default MediaUploadDialog;
diff --git a/src/pages/media_uploads/edit-media-upload-page.js b/src/pages/media_uploads/edit-media-upload-page.js
deleted file mode 100644
index 6e866795e..000000000
--- a/src/pages/media_uploads/edit-media-upload-page.js
+++ /dev/null
@@ -1,96 +0,0 @@
-/**
- * Copyright 2020 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 { connect } from "react-redux";
-import T from "i18n-react/dist/i18n-react";
-import { Breadcrumb } from "react-breadcrumbs";
-import MediaUploadForm from "../../components/forms/media-upload-form";
-import {
- getMediaUpload,
- resetMediaUploadForm,
- saveMediaUpload
-} from "../../actions/media-upload-actions";
-import { getAllMediaFileTypes } from "../../actions/media-file-type-actions";
-import AddNewButton from "../../components/buttons/add-new-button";
-
-// import '../../styles/edit-media-upload-page.less';
-
-class EditMediaUploadPage extends React.Component {
- constructor(props) {
- const mediaUploadId = props.match.params.media_upload_id;
- super(props);
-
- this.state = {};
-
- if (!mediaUploadId) {
- props.resetMediaUploadForm();
- } else {
- props.getMediaUpload(mediaUploadId);
- }
-
- props.getAllMediaFileTypes();
- }
-
- componentDidUpdate(prevProps, prevState, snapshot) {
- const oldId = prevProps.match.params.media_upload_id;
- const newId = this.props.match.params.media_upload_id;
-
- if (oldId !== newId) {
- if (!newId) {
- this.props.resetTemplateForm();
- } else {
- this.props.getMediaUpload(newId);
- }
- }
- }
-
- render() {
- const { entity, errors, match, currentSummit, media_file_types } =
- this.props;
- const title = entity.id
- ? T.translate("general.edit")
- : T.translate("general.add");
- const breadcrumb = entity.id ? entity.name : T.translate("general.new");
-
- return (
-
-
-
- {title} {T.translate("media_upload.media_upload")}
-
-
-
-
-
- );
- }
-}
-
-const mapStateToProps = ({ mediaUploadState, currentSummitState }) => ({
- currentSummit: currentSummitState.currentSummit,
- ...mediaUploadState
-});
-
-export default connect(mapStateToProps, {
- getMediaUpload,
- resetMediaUploadForm,
- saveMediaUpload,
- getAllMediaFileTypes
-})(EditMediaUploadPage);
diff --git a/src/pages/media_uploads/media-upload-list-page.js b/src/pages/media_uploads/media-upload-list-page.js
index 3299f61e8..9e094d5d1 100644
--- a/src/pages/media_uploads/media-upload-list-page.js
+++ b/src/pages/media_uploads/media-upload-list-page.js
@@ -11,171 +11,270 @@
* limitations under the License.
* */
-import React, { useEffect } from "react";
+import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
+import PropTypes from "prop-types";
import T from "i18n-react/dist/i18n-react";
-import Swal from "sweetalert2";
-import { Pagination } from "react-bootstrap";
-import FreeTextSearch from "openstack-uicore-foundation/lib/components/free-text-search"
-import Table from "openstack-uicore-foundation/lib/components/table";
-import { getSummitById } from "../../actions/summit-actions";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Grid2 from "@mui/material/Grid2";
+import AddIcon from "@mui/icons-material/Add";
+import MuiTable from "openstack-uicore-foundation/lib/components/mui/table";
+import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-input";
+import SummitDropdown from "../../components/summit-dropdown";
import {
- getMediaUploads,
- deleteMediaUpload,
- copyMediaUploads
+ getMediaUploads as getMediaUploadsAction,
+ getMediaUpload as getMediaUploadAction,
+ deleteMediaUpload as deleteMediaUploadAction,
+ copyMediaUploads as copyMediaUploadsAction,
+ resetMediaUploadForm as resetMediaUploadFormAction,
+ saveMediaUpload as saveMediaUploadAction
} from "../../actions/media-upload-actions";
-import SummitDropdown from "../../components/summit-dropdown";
+import { getAllMediaFileTypes as getAllMediaFileTypesAction } from "../../actions/media-file-type-actions";
+import { DEFAULT_CURRENT_PAGE } from "../../utils/constants";
+import MediaUploadDialog from "./components/media-upload-dialog";
const MediaUploadListPage = ({
- history,
currentSummit,
media_uploads,
+ currentMediaUpload,
+ currentMediaUploadErrors,
+ mediaFileTypes,
term,
- order,
- orderDir,
currentPage,
- lastPage,
perPage,
- ...props
+ order,
+ orderDir,
+ totalMediaUploads,
+ getMediaUploads,
+ getMediaUpload,
+ deleteMediaUpload,
+ copyMediaUploads,
+ resetMediaUploadForm,
+ saveMediaUpload,
+ getAllMediaFileTypes
}) => {
+ const [openPopup, setOpenPopup] = useState(null);
+
useEffect(() => {
- props.getMediaUploads();
+ getMediaUploads();
+ getAllMediaFileTypes();
}, []);
- const handleEdit = (media_upload_id) => {
- history.push(
- `/app/summits/${currentSummit.id}/media-uploads/${media_upload_id}`
- );
+ const handleEdit = (row) => {
+ getMediaUpload(row.id).then(() => setOpenPopup("mediaUploadForm"));
};
- const handlePageChange = (page) => {
- props.getMediaUploads(term, page, perPage, order, orderDir);
+ const handleNew = () => {
+ resetMediaUploadForm();
+ setOpenPopup("mediaUploadForm");
};
- const handleSort = (index, key, dir) => {
- props.getMediaUploads(term, currentPage, perPage, key, dir);
+ // The dialog closes when this promise resolves, so it must track ONLY the
+ // save: a failed list refresh after a successful create would otherwise keep
+ // the dialog open and invite a duplicate-create retry.
+ const handleSave = (mediaUploadEntity) =>
+ saveMediaUpload(mediaUploadEntity).then(() => {
+ getMediaUploads(
+ term,
+ DEFAULT_CURRENT_PAGE,
+ perPage,
+ order,
+ orderDir
+ ).catch(() => {});
+ });
+
+ const handleDelete = (mediaUploadId) => {
+ deleteMediaUpload(mediaUploadId).then(() =>
+ getMediaUploads(term, DEFAULT_CURRENT_PAGE, perPage, order, orderDir)
+ );
};
- const handleSearch = (term) => {
- props.getMediaUploads(term, currentPage, perPage, order, orderDir);
+ const handleSearch = (searchTerm) => {
+ getMediaUploads(searchTerm, DEFAULT_CURRENT_PAGE, perPage, order, orderDir);
};
- const handleNewMediaUpload = (ev) => {
- ev.preventDefault();
- history.push(`/app/summits/${currentSummit.id}/media-uploads/new`);
+ const handlePageChange = (page) => {
+ getMediaUploads(term, page, perPage, order, orderDir);
};
- const handleDelete = (mediaUploadId) => {
- const media_upload = media_uploads.find((t) => t.id === mediaUploadId);
-
- Swal.fire({
- title: T.translate("general.are_you_sure"),
- text: `${T.translate("media_upload.delete_warning")} ${
- media_upload.name
- }}`,
- type: "warning",
- showCancelButton: true,
- confirmButtonColor: "#DD6B55",
- confirmButtonText: T.translate("general.yes_delete")
- }).then((result) => {
- if (result.value) {
- props.deleteMediaUpload(mediaUploadId);
- }
- });
+ const handlePerPageChange = (newPerPage) => {
+ getMediaUploads(term, DEFAULT_CURRENT_PAGE, newPerPage, order, orderDir);
+ };
+
+ const handleSort = (key, dir) => {
+ getMediaUploads(term, DEFAULT_CURRENT_PAGE, perPage, key, dir);
};
const handleCopyMediaUploads = (fromSummitId) => {
- props.copyMediaUploads(fromSummitId);
+ copyMediaUploads(fromSummitId);
};
- const canEdit = (item) => !item.is_system_defined;
+ const canDeleteMediaUpload = (row) => !row.is_system_defined;
const columns = [
- { columnKey: "id", value: T.translate("general.id"), sortable: true },
+ { columnKey: "id", header: T.translate("general.id"), sortable: true },
{
columnKey: "name",
- value: T.translate("media_upload.name"),
+ header: T.translate("media_upload.name"),
sortable: true
},
{
columnKey: "description",
- value: T.translate("media_upload.description")
+ header: T.translate("media_upload.description")
}
];
- const table_options = {
- sortCol: order,
- sortDir: orderDir,
- actions: {
- edit: { onClick: handleEdit },
- delete: { onClick: handleDelete, display: canEdit }
- }
- };
+ const tableOptions = { sortCol: order, sortDir: orderDir };
+
+ if (!currentSummit.id) return ;
return (
-
{T.translate("media_upload.media_upload_list")}
-
-
-
-
-
-
+
{T.translate("media_upload.media_upload_list")}
+
+
+
+ {totalMediaUploads} {T.translate("media_upload.media_uploads")}
+
+
+
+
+
+
-
-
+
}
+ sx={{
+ height: "36px",
+ padding: "6px 16px",
+ fontSize: "1.4rem",
+ lineHeight: "2.4rem",
+ letterSpacing: "0.4px"
+ }}
+ >
+ {T.translate("media_upload.add")}
+
+
+
+
+ {media_uploads.length > 0 && (
+
+ `${T.translate("media_upload.delete_warning")}${name}`
+ }
+ confirmButtonColor="error"
+ />
+ )}
{media_uploads.length === 0 && (
{T.translate("media_upload.no_results")}
)}
- {media_uploads.length > 0 && (
-
+ {openPopup === "mediaUploadForm" && (
+ setOpenPopup(null)}
+ onSave={handleSave}
+ />
)}
);
};
-const mapStateToProps = ({ currentSummitState, mediaUploadListState }) => ({
+MediaUploadListPage.propTypes = {
+ currentSummit: PropTypes.shape({ id: PropTypes.number }).isRequired,
+ media_uploads: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.number,
+ name: PropTypes.string,
+ description: PropTypes.string
+ })
+ ).isRequired,
+ currentMediaUpload: PropTypes.shape({ id: PropTypes.number }).isRequired,
+ currentMediaUploadErrors: PropTypes.shape({}),
+ mediaFileTypes: PropTypes.arrayOf(PropTypes.shape({})),
+ term: PropTypes.string,
+ currentPage: PropTypes.number,
+ perPage: PropTypes.number,
+ order: PropTypes.string,
+ orderDir: PropTypes.number,
+ totalMediaUploads: PropTypes.number,
+ getMediaUploads: PropTypes.func.isRequired,
+ getMediaUpload: PropTypes.func.isRequired,
+ deleteMediaUpload: PropTypes.func.isRequired,
+ copyMediaUploads: PropTypes.func.isRequired,
+ resetMediaUploadForm: PropTypes.func.isRequired,
+ saveMediaUpload: PropTypes.func.isRequired,
+ getAllMediaFileTypes: PropTypes.func.isRequired
+};
+
+MediaUploadListPage.defaultProps = {
+ currentMediaUploadErrors: {},
+ mediaFileTypes: [],
+ term: "",
+ currentPage: 1,
+ perPage: 10,
+ order: "id",
+ orderDir: 1,
+ totalMediaUploads: 0
+};
+
+const mapStateToProps = ({
+ currentSummitState,
+ mediaUploadListState,
+ mediaUploadState
+}) => ({
currentSummit: currentSummitState.currentSummit,
- ...mediaUploadListState
+ ...mediaUploadListState,
+ currentMediaUpload: mediaUploadState.entity,
+ currentMediaUploadErrors: mediaUploadState.errors,
+ mediaFileTypes: mediaUploadState.media_file_types
});
export default connect(mapStateToProps, {
- getSummitById,
- getMediaUploads,
- deleteMediaUpload,
- copyMediaUploads
+ getMediaUploads: getMediaUploadsAction,
+ getMediaUpload: getMediaUploadAction,
+ deleteMediaUpload: deleteMediaUploadAction,
+ copyMediaUploads: copyMediaUploadsAction,
+ resetMediaUploadForm: resetMediaUploadFormAction,
+ saveMediaUpload: saveMediaUploadAction,
+ getAllMediaFileTypes: getAllMediaFileTypesAction
})(MediaUploadListPage);
diff --git a/src/reducers/media_uploads/media-upload-reducer.js b/src/reducers/media_uploads/media-upload-reducer.js
index 9de06ca9b..a7dba94a4 100644
--- a/src/reducers/media_uploads/media-upload-reducer.js
+++ b/src/reducers/media_uploads/media-upload-reducer.js
@@ -79,7 +79,8 @@ const mediaUploadReducer = (state = DEFAULT_STATE, action) => {
return {
...state,
entity: { ...DEFAULT_ENTITY, ...entity },
- preview: null
+ preview: null,
+ errors: {}
};
}
case VALIDATE: {
diff --git a/src/reducers/media_uploads/media-uploads-list-reducer.js b/src/reducers/media_uploads/media-uploads-list-reducer.js
index e196c81f6..c1d6315a9 100644
--- a/src/reducers/media_uploads/media-uploads-list-reducer.js
+++ b/src/reducers/media_uploads/media-uploads-list-reducer.js
@@ -26,7 +26,8 @@ const DEFAULT_STATE = {
orderDir: 1,
currentPage: 1,
lastPage: 1,
- perPage: 10
+ perPage: 10,
+ totalMediaUploads: 0
};
const mediaUploadListReducer = (state = DEFAULT_STATE, action = {}) => {
@@ -37,13 +38,16 @@ const mediaUploadListReducer = (state = DEFAULT_STATE, action = {}) => {
return DEFAULT_STATE;
}
case REQUEST_MEDIA_UPLOADS: {
- const { order, orderDir, term } = payload;
+ const { order, orderDir, term, perPage } = payload;
- return { ...state, order, orderDir, term };
+ return { ...state, order, orderDir, term, perPage };
}
case RECEIVE_MEDIA_UPLOADS: {
- const { last_page: lastPage, current_page: currentPage } =
- payload.response;
+ const {
+ total,
+ last_page: lastPage,
+ current_page: currentPage
+ } = payload.response;
const media_uploads = payload.response.data.map((mft) => ({
id: mft.id,
name: mft.name,
@@ -54,7 +58,8 @@ const mediaUploadListReducer = (state = DEFAULT_STATE, action = {}) => {
...state,
media_uploads,
currentPage,
- lastPage
+ lastPage,
+ totalMediaUploads: total
};
}
case MEDIA_UPLOAD_DELETED: {
diff --git a/yarn.lock b/yarn.lock
index d21422dac..35d54c63d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9068,10 +9068,10 @@ open@^10.0.3:
is-inside-container "^1.0.0"
wsl-utils "^0.1.0"
-openstack-uicore-foundation@5.0.38:
- version "5.0.38"
- resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.38.tgz#54feaacc2a34d56a3db59b48bd2572bf25e963cf"
- integrity sha512-MQCh+JOte0afEeWv5tfC6P3bkx71sp31qAdMjtnim1MkggP+hpFUwlUo6ArUZ9b/ksEQvyZHLI0eveSB5O6cRg==
+openstack-uicore-foundation@5.0.39:
+ version "5.0.39"
+ resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.39.tgz#8686cec9576056655b68a1ac0b9a56b4cfa68adf"
+ integrity sha512-/O6yyD0JXIyHnYP+3gTuQ844uNqp70vo/mmNMXa3U8cPQEwhN9YFNVlvzGYRDMpQ84L+Rum1VrgbgsMyBJlyRA==
dependencies:
use-sync-external-store "^1.6.0"