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 ( - - + - {title} - - ({ - position: "absolute", - right: 12, - top: 12, - color: theme.palette.grey[500] - })} - > - - - - - {!url || imageError ? ( - - ) : ( - setImageError(true)} - sx={{ - maxWidth: "100%", - maxHeight: 400, - display: "block", - objectFit: "contain" - }} - /> - )} + {!url || imageError ? ( + + ) : ( + setImageError(true)} + sx={{ + maxWidth: "100%", + maxHeight: 400, + display: "block", + objectFit: "contain" + }} + /> + )} + + {filename && ( + + + {T.translate("preview_modal.file_name")} + + + {filename} + - - {filename && ( - - - {T.translate("preview_modal.file_name")} - - - {filename} - - - )} - {!!uploadDate && ( - - - {T.translate("preview_modal.uploaded")} - - {formatDate(uploadDate)} - - )} + )} + {!!uploadDate && ( + + + {T.translate("preview_modal.uploaded")} + + {formatDate(uploadDate)} - - + )} + ); }; 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 ( - - - {T.translate("edit_sponsor.mu_tab.upload_input.upload_file")} - - ({ - position: "absolute", - right: 8, - top: 8, - color: theme.palette.grey[500] - })} - > - - - - - - {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 - /> - )} - - - - - + + + {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"