diff --git a/package.json b/package.json
index 849620011..cca05f3cf 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.47",
+ "openstack-uicore-foundation": "5.0.50",
"p-limit": "^6.1.0",
"path-browserify": "^1.0.1",
"postcss-loader": "^6.2.1",
diff --git a/src/actions/sponsor-mu-actions.js b/src/actions/sponsor-mu-actions.js
index f8ef47791..a486a3cfb 100644
--- a/src/actions/sponsor-mu-actions.js
+++ b/src/actions/sponsor-mu-actions.js
@@ -178,3 +178,62 @@ export const removeFileForSponsorMU =
dispatch(stopLoading());
});
};
+
+export const uploadTextForSponsorMU =
+ (pageId, moduleId, text) => async (dispatch, getState) => {
+ const { currentSummitState, currentSponsorState } = getState();
+ const { currentSummit } = currentSummitState;
+ const { entity: sponsor } = currentSponsorState;
+ const accessToken = await getAccessTokenSafely();
+
+ dispatch(startLoading());
+
+ const params = {
+ access_token: accessToken
+ };
+
+ return putRequest(
+ null,
+ createAction("DUMMY_ACTION"),
+ `${window.SPONSOR_PAGES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/available-pages/${pageId}/modules/${moduleId}/text`,
+ { value: text },
+ snackbarErrorHandler
+ )(params)(dispatch)
+ .then(({ response }) => {
+ dispatch(
+ createAction(SPONSOR_MEDIA_UPLOAD_FILE_UPLOADED)({
+ ...response,
+ moduleId
+ })
+ );
+ })
+ .finally(() => {
+ dispatch(stopLoading());
+ });
+ };
+
+export const removeTextForSponsorMU =
+ (pageId, moduleId) => async (dispatch, getState) => {
+ const { currentSummitState, currentSponsorState } = getState();
+ const { currentSummit } = currentSummitState;
+ const { entity: sponsor } = currentSponsorState;
+ const accessToken = await getAccessTokenSafely();
+
+ dispatch(startLoading());
+
+ const params = {
+ access_token: accessToken
+ };
+
+ return deleteRequest(
+ null,
+ createAction(SPONSOR_MEDIA_UPLOAD_FILE_DELETED)({ moduleId }),
+ `${window.SPONSOR_PAGES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/available-pages/${pageId}/modules/${moduleId}/text`,
+ null,
+ snackbarErrorHandler
+ )(params)(dispatch)
+ .catch(() => {})
+ .finally(() => {
+ dispatch(stopLoading());
+ });
+ };
diff --git a/src/components/mui/PreviewModal/index.jsx b/src/components/mui/PreviewModal/index.jsx
index 23b038794..5dfd0e2ed 100644
--- a/src/components/mui/PreviewModal/index.jsx
+++ b/src/components/mui/PreviewModal/index.jsx
@@ -1,15 +1,8 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import T from "i18n-react/dist/i18n-react";
-import {
- Dialog,
- DialogTitle,
- DialogContent,
- Box,
- Typography,
- IconButton
-} from "@mui/material";
-import CloseIcon from "@mui/icons-material/Close";
+import { Box, Typography } from "@mui/material";
+import CustomDialog from "openstack-uicore-foundation/lib/components/mui/custom-dialog";
import BrokenImageOutlinedIcon from "@mui/icons-material/BrokenImageOutlined";
import { formatDate } from "../../../utils/methods";
@@ -35,87 +28,55 @@ const PreviewModal = ({ title, open, onClose, url, filename, uploadDate }) => {
}, [open]);
return (
-
+ )}
+
);
};
diff --git a/src/components/upload-dialog/index.js b/src/components/upload-dialog/index.js
index a9dcd8ccf..718a6faf1 100644
--- a/src/components/upload-dialog/index.js
+++ b/src/components/upload-dialog/index.js
@@ -12,24 +12,14 @@
* */
import React, { useState } from "react";
-import {
- Box,
- Button,
- Dialog,
- DialogContent,
- DialogTitle,
- Divider,
- IconButton,
- Typography
-} from "@mui/material";
+import { Box, Divider, IconButton, Typography } from "@mui/material";
import PropTypes from "prop-types";
import UploadInputV3 from "openstack-uicore-foundation/lib/components/inputs/upload-input-v3";
+import CustomDialog from "openstack-uicore-foundation/lib/components/mui/custom-dialog";
import T from "i18n-react/dist/i18n-react";
-import CloseIcon from "@mui/icons-material/Close";
import NoteAddIcon from "@mui/icons-material/NoteAdd";
import DeleteIcon from "@mui/icons-material/Delete";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
-import DialogActions from "@mui/material/DialogActions";
const MAX_PAGE_MODULE_UPLOAD_QTY = 1;
@@ -90,9 +80,7 @@ const UploadDialog = ({
}
};
- const handleUpload = () => {
- onUpload(uploadedFile);
- };
+ const handleUpload = () => onUpload(uploadedFile);
const handleRemove = () => {
if (onRemove) {
@@ -108,62 +96,43 @@ const UploadDialog = ({
const canAddMore = () => (value?.length || 0) < maxFiles;
return (
-
+
+
+ {fileMeta.name}
+
+
+ {fileMeta.description}
+
+ {value ? (
+ <>
+
+
+ >
+ ) : (
+ setUploadedFile(null)}
+ postUrl={`${window.FILE_UPLOAD_API_BASE_URL}/api/v1/files/upload`}
+ djsConfig={{ withCredentials: true }}
+ maxFiles={maxFiles}
+ canAdd={canAddMore()}
+ parallelChunkUploads
+ />
+ )}
+
);
};
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 58eb3a2c0..0836c5b2d 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -2721,6 +2721,7 @@
"media_upload": "media upload",
"sponsor_request": "Sponsor Specific Request",
"general_request": "General Media Request",
+ "type": "Type",
"add_on": "Add-on",
"max_size": "Max Size",
"format": "Format",
@@ -2728,7 +2729,9 @@
"status": "Status",
"upload_input": {
"complete": "Complete",
- "upload_file": "Upload file"
+ "upload_file": "Upload file",
+ "enter_text": "Enter text",
+ "save_answer": "Save Answer"
}
},
"placeholders": {
diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/__tests__/index.test.jsx b/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/__tests__/index.test.jsx
new file mode 100644
index 000000000..ec1a1cc13
--- /dev/null
+++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/__tests__/index.test.jsx
@@ -0,0 +1,276 @@
+import React from "react";
+import { screen, within, act } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import "@testing-library/jest-dom";
+import { renderWithRedux } from "../../../../../../utils/test-utils";
+import SponsorMediaUploadTab from "../index";
+import {
+ uploadFileForSponsorMU,
+ removeFileForSponsorMU,
+ uploadTextForSponsorMU,
+ removeTextForSponsorMU
+} from "../../../../../../actions/sponsor-mu-actions";
+
+jest.mock("../../../../../../actions/sponsor-mu-actions", () => ({
+ getSponsorMURequests: jest.fn(() => () => Promise.resolve()),
+ getGeneralMURequests: jest.fn(() => () => Promise.resolve()),
+ uploadFileForSponsorMU: jest.fn(() => () => Promise.resolve()),
+ removeFileForSponsorMU: jest.fn(() => () => Promise.resolve()),
+ uploadTextForSponsorMU: jest.fn(() => () => Promise.resolve()),
+ removeTextForSponsorMU: jest.fn(() => () => Promise.resolve())
+}));
+
+jest.mock("../../../../../../components/mui/showConfirmDialog", () => ({
+ __esModule: true,
+ default: jest.fn(() => Promise.resolve(true))
+}));
+
+jest.mock("../../../../../../components/upload-dialog", () => ({
+ __esModule: true,
+ default: ({ open, onUpload, onClose }) =>
+ open ? (
+
+
+
+
+ ) : null
+}));
+
+jest.mock("../components/text-value-dialog", () => ({
+ __esModule: true,
+ default: ({ open, onSubmit, onClose }) =>
+ open ? (
+
+
+
+
+ ) : null
+}));
+
+jest.mock("../../../../../../components/mui/PreviewModal", () => ({
+ __esModule: true,
+ default: ({ open, url }) =>
+ open ? {url}
: null
+}));
+
+jest.mock("../components/text-preview-modal", () => ({
+ __esModule: true,
+ default: ({ open, value }) =>
+ open ? {value}
: null
+}));
+
+jest.mock("openstack-uicore-foundation/lib/components/mui/table", () => ({
+ __esModule: true,
+ default: ({ columns, data }) => (
+
+ {data.map((row) => (
+
+ {columns.map((col) => (
+
+ {col.render ? col.render(row) : row[col.columnKey]}
+
+ ))}
+
+ ))}
+
+ )
+}));
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key) => key }
+}));
+
+const fileRowWithValue = {
+ id: 1,
+ page_id: 10,
+ name: "Logo",
+ mu_type: "file",
+ status: "Complete",
+ media_upload: {
+ file_mimetype: "image/png",
+ public_url: "https://example.com/logo.png",
+ file_name: "logo.png",
+ file_created: 123
+ }
+};
+
+const fileRowWithoutValue = {
+ id: 2,
+ page_id: 10,
+ name: "Brochure",
+ mu_type: "file",
+ status: "Pending",
+ media_upload: null
+};
+
+const textRowWithValue = {
+ id: 3,
+ page_id: 10,
+ name: "Company Bio",
+ mu_type: "text",
+ status: "Complete",
+ media_upload: { value: "Sponsor's bio text" }
+};
+
+const textRowWithoutValue = {
+ id: 4,
+ page_id: 10,
+ name: "Tagline",
+ mu_type: "text",
+ status: "Pending",
+ media_upload: null
+};
+
+const buildInitialState = (requests) => ({
+ sponsorPageMUListState: {
+ sponsorRequests: {
+ requests,
+ order: "name",
+ orderDir: 1,
+ currentPage: 1,
+ lastPage: 1,
+ perPage: 10,
+ totalCount: requests.length
+ },
+ generalRequests: {
+ requests: [],
+ order: "name",
+ orderDir: 1,
+ currentPage: 1,
+ lastPage: 1,
+ perPage: 10,
+ totalCount: 0
+ },
+ summitTZ: ""
+ },
+ currentSponsorState: { entity: { id: 99 } }
+});
+
+const getCell = (rowId, columnKey) =>
+ screen.getByTestId(`cell-${rowId}-${columnKey}`);
+
+describe("SponsorMediaUploadTab", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.spyOn(window, "open").mockImplementation(() => {});
+ });
+
+ it("enables view but keeps download disabled for a text row with a saved value, and previews the text", async () => {
+ renderWithRedux(, {
+ initialState: buildInitialState([textRowWithValue])
+ });
+
+ const viewButton = within(getCell(3, "view")).getByRole("button");
+ const downloadButton = within(getCell(3, "download")).getByRole("button");
+
+ expect(viewButton).toBeEnabled();
+ expect(downloadButton).toBeDisabled();
+
+ await userEvent.click(viewButton);
+
+ expect(screen.getByTestId("text-preview-modal")).toHaveTextContent(
+ "Sponsor's bio text"
+ );
+ expect(screen.queryByTestId("preview-modal")).not.toBeInTheDocument();
+ });
+
+ it("disables view and download for a text row with no saved value", () => {
+ renderWithRedux(, {
+ initialState: buildInitialState([textRowWithoutValue])
+ });
+
+ expect(within(getCell(4, "view")).getByRole("button")).toBeDisabled();
+ expect(within(getCell(4, "download")).getByRole("button")).toBeDisabled();
+ });
+
+ it("opens the text dialog for an empty text row and submits via uploadTextForSponsorMU", async () => {
+ renderWithRedux(, {
+ initialState: buildInitialState([textRowWithoutValue])
+ });
+
+ await userEvent.click(
+ within(getCell(4, "upload_delete")).getByRole("button")
+ );
+
+ expect(screen.getByTestId("text-value-dialog")).toBeInTheDocument();
+
+ await act(async () => {
+ await userEvent.click(
+ screen.getByRole("button", { name: "submit-text" })
+ );
+ });
+
+ expect(uploadTextForSponsorMU).toHaveBeenCalledWith(10, 4, "new answer");
+ });
+
+ it("deletes a text row with a saved value via removeTextForSponsorMU", async () => {
+ renderWithRedux(, {
+ initialState: buildInitialState([textRowWithValue])
+ });
+
+ await act(async () => {
+ await userEvent.click(
+ within(getCell(3, "upload_delete")).getByRole("button")
+ );
+ });
+
+ expect(removeTextForSponsorMU).toHaveBeenCalledWith(10, 3);
+ expect(removeFileForSponsorMU).not.toHaveBeenCalled();
+ });
+
+ it("keeps File-row behavior unchanged: image preview, public_url download, upload dialog, file delete", async () => {
+ renderWithRedux(, {
+ initialState: buildInitialState([fileRowWithValue, fileRowWithoutValue])
+ });
+
+ expect(within(getCell(1, "view")).getByRole("button")).toBeEnabled();
+ expect(within(getCell(1, "download")).getByRole("button")).toBeEnabled();
+
+ await userEvent.click(within(getCell(1, "view")).getByRole("button"));
+ expect(screen.getByTestId("preview-modal")).toHaveTextContent(
+ "https://example.com/logo.png"
+ );
+ expect(screen.queryByTestId("text-preview-modal")).not.toBeInTheDocument();
+
+ await userEvent.click(within(getCell(1, "download")).getByRole("button"));
+ expect(window.open).toHaveBeenCalledWith(
+ "https://example.com/logo.png",
+ "_blank",
+ "noopener,noreferrer"
+ );
+
+ await act(async () => {
+ await userEvent.click(
+ within(getCell(1, "upload_delete")).getByRole("button")
+ );
+ });
+ expect(removeFileForSponsorMU).toHaveBeenCalledWith(10, 1);
+ expect(removeTextForSponsorMU).not.toHaveBeenCalled();
+
+ await userEvent.click(
+ within(getCell(2, "upload_delete")).getByRole("button")
+ );
+ expect(screen.getByTestId("upload-dialog")).toBeInTheDocument();
+
+ await act(async () => {
+ await userEvent.click(
+ screen.getByRole("button", { name: "submit-file" })
+ );
+ });
+ expect(uploadFileForSponsorMU).toHaveBeenCalledWith(10, 2, {
+ name: "file.pdf"
+ });
+ });
+});
diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/components/text-preview-modal/__tests__/index.test.jsx b/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/components/text-preview-modal/__tests__/index.test.jsx
new file mode 100644
index 000000000..ab9734c4b
--- /dev/null
+++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/components/text-preview-modal/__tests__/index.test.jsx
@@ -0,0 +1,68 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import "@testing-library/jest-dom";
+import TextPreviewModal from "../index";
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key) => key }
+}));
+
+describe("TextPreviewModal", () => {
+ const onClose = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ Object.defineProperty(navigator, "clipboard", {
+ value: { writeText: jest.fn() },
+ configurable: true
+ });
+ });
+
+ it("renders the title and the stored text value", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("Company Bio")).toBeInTheDocument();
+ expect(
+ screen.getByText("This is the sponsor's answer")
+ ).toBeInTheDocument();
+ });
+
+ it("copies the value to the clipboard when the copy button is clicked", async () => {
+ render(
+
+ );
+
+ const copyButton = screen.getByRole("button", {
+ name: "general.copy_to_clipboard"
+ });
+
+ await userEvent.click(copyButton);
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith("Copy me");
+ expect(copyButton).toHaveTextContent("general.copied");
+ });
+
+ it("calls onClose when closing", async () => {
+ render(
+
+ );
+
+ await userEvent.click(screen.getByRole("button", { name: "close" }));
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/components/text-preview-modal/index.jsx b/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/components/text-preview-modal/index.jsx
new file mode 100644
index 000000000..b5bce412f
--- /dev/null
+++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/components/text-preview-modal/index.jsx
@@ -0,0 +1,68 @@
+import React, { useEffect, useState } from "react";
+import PropTypes from "prop-types";
+import T from "i18n-react/dist/i18n-react";
+import { Box, Typography } from "@mui/material";
+import CustomDialog from "openstack-uicore-foundation/lib/components/mui/custom-dialog";
+import { MILLISECONDS_IN_SECOND } from "../../../../../../../utils/constants";
+
+const TextPreviewModal = ({ title, open, onClose, value }) => {
+ const [copyLabel, setCopyLabel] = useState(
+ T.translate("general.copy_to_clipboard")
+ );
+
+ useEffect(() => {
+ if (open) {
+ setCopyLabel(T.translate("general.copy_to_clipboard"));
+ }
+ }, [open]);
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(value);
+ setCopyLabel(T.translate("general.copied"));
+ setTimeout(() => {
+ setCopyLabel(T.translate("general.copy_to_clipboard"));
+ }, MILLISECONDS_IN_SECOND);
+ };
+
+ return (
+
+
+
+ {value}
+
+
+
+ );
+};
+
+TextPreviewModal.propTypes = {
+ title: PropTypes.string.isRequired,
+ open: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+ value: PropTypes.string
+};
+
+TextPreviewModal.defaultProps = {
+ value: ""
+};
+
+export default TextPreviewModal;
diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/components/text-value-dialog/__tests__/index.test.jsx b/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/components/text-value-dialog/__tests__/index.test.jsx
new file mode 100644
index 000000000..cf0208668
--- /dev/null
+++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/components/text-value-dialog/__tests__/index.test.jsx
@@ -0,0 +1,76 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import "@testing-library/jest-dom";
+import TextValueDialog from "../index";
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key) => key }
+}));
+
+describe("TextValueDialog", () => {
+ const onClose = jest.fn();
+ const onSubmit = jest.fn(() => Promise.resolve());
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("renders an empty text field with a disabled submit button when there is no value", () => {
+ render(
+
+ );
+
+ expect(screen.getByRole("textbox")).toHaveValue("");
+ expect(
+ screen.getByRole("button", {
+ name: "edit_sponsor.mu_tab.upload_input.save_answer"
+ })
+ ).toBeDisabled();
+ });
+
+ it("enables submit once text is typed and calls onSubmit with it", async () => {
+ render(
+
+ );
+
+ await userEvent.type(screen.getByRole("textbox"), "Hello sponsor");
+
+ const submitButton = screen.getByRole("button", {
+ name: "edit_sponsor.mu_tab.upload_input.save_answer"
+ });
+ expect(submitButton).toBeEnabled();
+
+ await userEvent.click(submitButton);
+
+ expect(onSubmit).toHaveBeenCalledWith("Hello sponsor");
+ });
+
+ it("calls onClose when closing", async () => {
+ render(
+
+ );
+
+ await userEvent.click(screen.getByRole("button", { name: "close" }));
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/components/text-value-dialog/index.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/components/text-value-dialog/index.js
new file mode 100644
index 000000000..908f63943
--- /dev/null
+++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/components/text-value-dialog/index.js
@@ -0,0 +1,65 @@
+/**
+ * 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, { useState } from "react";
+import { TextField, Typography } from "@mui/material";
+import PropTypes from "prop-types";
+import T from "i18n-react/dist/i18n-react";
+import CustomDialog from "openstack-uicore-foundation/lib/components/mui/custom-dialog";
+import { TEXT_MAX_LENGTH_1024 } from "../../../../../../../utils/constants";
+
+const TextValueDialog = ({ name, moduleName, open, onClose, onSubmit }) => {
+ const [text, setText] = useState("");
+
+ return (
+ onSubmit(text).then(() => onClose()),
+ disabled: text === ""
+ }}
+ >
+
+ {moduleName}
+
+ setText(ev.target.value)}
+ inputProps={{ maxLength: TEXT_MAX_LENGTH_1024 }}
+ helperText={`${text.length}/${TEXT_MAX_LENGTH_1024}`}
+ />
+
+ );
+};
+
+TextValueDialog.propTypes = {
+ open: PropTypes.bool.isRequired,
+ name: PropTypes.string.isRequired,
+ moduleName: PropTypes.string,
+ onClose: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired
+};
+
+TextValueDialog.defaultProps = {
+ moduleName: ""
+};
+
+export default TextValueDialog;
diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/index.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/index.js
index 3488daf48..f4d8d5fc6 100644
--- a/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/index.js
+++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-media-upload-tab/index.js
@@ -24,13 +24,17 @@ import {
getGeneralMURequests,
getSponsorMURequests,
removeFileForSponsorMU,
- uploadFileForSponsorMU
+ uploadFileForSponsorMU,
+ uploadTextForSponsorMU,
+ removeTextForSponsorMU
} from "../../../../../actions/sponsor-mu-actions";
import CustomAlert from "../../../../../components/mui/custom-alert";
import { SPONSOR_MEDIA_UPLOAD_STATUS } from "../../../../../utils/constants";
import UploadDialog from "../../../../../components/upload-dialog";
+import TextValueDialog from "./components/text-value-dialog";
import showConfirmDialog from "../../../../../components/mui/showConfirmDialog";
import PreviewModal from "../../../../../components/mui/PreviewModal";
+import TextPreviewModal from "./components/text-preview-modal";
const SponsorMediaUploadTab = ({
sponsor,
@@ -39,7 +43,9 @@ const SponsorMediaUploadTab = ({
getSponsorMURequests,
getGeneralMURequests,
uploadFileForSponsorMU,
- removeFileForSponsorMU
+ removeFileForSponsorMU,
+ uploadTextForSponsorMU,
+ removeTextForSponsorMU
}) => {
const [uploadModule, setUploadModule] = useState(null);
const [previewModule, setPreviewModule] = useState(null);
@@ -63,13 +69,19 @@ const SponsorMediaUploadTab = ({
setUploadModule(item);
};
- const handleUploadFile = (file) => {
+ const handleUploadFile = (file) =>
uploadFileForSponsorMU(uploadModule.page_id, uploadModule.id, file).then(
() => {
setUploadModule(null);
}
);
- };
+
+ const handleUploadText = (text) =>
+ uploadTextForSponsorMU(uploadModule.page_id, uploadModule.id, text).then(
+ () => {
+ setUploadModule(null);
+ }
+ );
const handleView = (item) => {
setPreviewModule(item);
@@ -96,7 +108,11 @@ const SponsorMediaUploadTab = ({
});
if (isConfirmed) {
- removeFileForSponsorMU(item.page_id, item.id);
+ if (item.mu_type === "text") {
+ removeTextForSponsorMU(item.page_id, item.id);
+ } else {
+ removeFileForSponsorMU(item.page_id, item.id);
+ }
}
};
@@ -135,6 +151,10 @@ const SponsorMediaUploadTab = ({
header: nameLabel,
sortable: true
},
+ {
+ columnKey: "mu_type",
+ header: T.translate("edit_sponsor.mu_tab.type")
+ },
{
columnKey: "add_on",
header: T.translate("edit_sponsor.mu_tab.add_on")
@@ -171,10 +191,16 @@ const SponsorMediaUploadTab = ({
align: "center",
render: (row) => {
const isImage = row.media_upload?.file_mimetype?.includes("image");
+ const isTextWithValue =
+ row.mu_type === "text" && !!row.media_upload?.value;
+ const viewDisabled =
+ row.mu_type === "text"
+ ? !isTextWithValue
+ : !row.media_upload || !isImage;
return (
handleView(row)}
>
@@ -190,7 +216,7 @@ const SponsorMediaUploadTab = ({
render: (row) => (
handleDownload(row)}
>
@@ -266,27 +292,46 @@ const SponsorMediaUploadTab = ({
onSort={handleGeneralSort}
/>
- setUploadModule(null)}
- onUpload={handleUploadFile}
- onRemove={() => handleDelete(uploadModule)}
- value={uploadModule?.media_upload}
- fileMeta={{
- ...(uploadModule?.file_type || {}),
- max_file_size: uploadModule?.max_file_size
- }}
- maxFiles={1}
- />
- setPreviewModule(null)}
- title={previewModule?.name}
- filename={previewModule?.media_upload?.file_name}
- uploadDate={previewModule?.media_upload?.file_created}
- url={previewModule?.media_upload?.public_url}
- />
+ {uploadModule?.mu_type === "text" ? (
+ setUploadModule(null)}
+ onSubmit={handleUploadText}
+ />
+ ) : (
+ setUploadModule(null)}
+ onUpload={handleUploadFile}
+ onRemove={() => handleDelete(uploadModule)}
+ value={uploadModule?.media_upload}
+ fileMeta={{
+ ...(uploadModule?.file_type || {}),
+ max_file_size: uploadModule?.max_file_size
+ }}
+ maxFiles={1}
+ />
+ )}
+ {previewModule?.mu_type === "text" ? (
+ setPreviewModule(null)}
+ title={previewModule?.name}
+ value={previewModule?.media_upload?.value}
+ />
+ ) : (
+ setPreviewModule(null)}
+ title={previewModule?.name}
+ filename={previewModule?.media_upload?.file_name}
+ uploadDate={previewModule?.media_upload?.file_created}
+ url={previewModule?.media_upload?.public_url}
+ />
+ )}
);
};
@@ -300,5 +345,7 @@ export default connect(mapStateToProps, {
getSponsorMURequests,
getGeneralMURequests,
uploadFileForSponsorMU,
- removeFileForSponsorMU
+ removeFileForSponsorMU,
+ uploadTextForSponsorMU,
+ removeTextForSponsorMU
})(SponsorMediaUploadTab);
diff --git a/src/reducers/sponsors/sponsor-page-mu-list-reducer.js b/src/reducers/sponsors/sponsor-page-mu-list-reducer.js
index 3d7453395..12c481c36 100644
--- a/src/reducers/sponsors/sponsor-page-mu-list-reducer.js
+++ b/src/reducers/sponsors/sponsor-page-mu-list-reducer.js
@@ -81,7 +81,9 @@ const mapMediaObject = (mediaObject) => {
max_size: `${bytesToMb(mediaObject.max_file_size)} MB`,
format: mediaObject.file_type?.allowed_extensions || "N/A",
deadline,
- status: getStatus(mediaObject)
+ status: getStatus(mediaObject),
+ // TODO: type should come from API
+ mu_type: mediaObject.file_type ? "file" : "text"
};
};
diff --git a/src/utils/constants.js b/src/utils/constants.js
index 5882abf4b..cc333f7d5 100644
--- a/src/utils/constants.js
+++ b/src/utils/constants.js
@@ -361,3 +361,5 @@ export const IMPORT_SPONSOR_USERS_STATUS = {
export const DEFAULT_REOPEN_HOURS = 24;
export const REOPEN_PRESET_HOURS_48 = 48;
export const REOPEN_PRESET_HOURS_72 = 72;
+
+export const TEXT_MAX_LENGTH_1024 = 1024;
diff --git a/yarn.lock b/yarn.lock
index 57c547313..90a8c4869 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9042,10 +9042,10 @@ open@^10.0.3:
is-inside-container "^1.0.0"
wsl-utils "^0.1.0"
-openstack-uicore-foundation@5.0.47:
- version "5.0.47"
- resolved "https://registry.npmjs.org/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.47.tgz#32500fb4d8392cbf5713e968afa875cea42a67a2"
- integrity sha512-E/6uO7Q5MtTfhWExshuNIrwVcxDewDvA76YmGzQz2f5LeZ8Gsv22eqBNbylDakwxuPmyD16r/AQEURr5soALAw==
+openstack-uicore-foundation@5.0.50:
+ version "5.0.50"
+ resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.50.tgz#e3212570a768c70e9add8dc717b4344a301c03f9"
+ integrity sha512-OpS73E7MWMeHcRF2qVBby2LmI3kt2bgBh4udXwc7VMv5UbCktla+hWsPVeHBIZLFExPT414pn7nt/DOkE0gYDQ==
dependencies:
use-sync-external-store "^1.6.0"