diff --git a/src/actions/page-template-actions.js b/src/actions/page-template-actions.js index 363230881..bd54f5e0e 100644 --- a/src/actions/page-template-actions.js +++ b/src/actions/page-template-actions.js @@ -12,6 +12,7 @@ * */ import T from "i18n-react/dist/i18n-react"; +import moment from "moment-timezone"; import { getRequest, putRequest, @@ -27,7 +28,8 @@ import { getAccessTokenSafely } from "../utils/methods"; import { DEFAULT_CURRENT_PAGE, DEFAULT_ORDER_DIR, - DEFAULT_PER_PAGE + DEFAULT_PER_PAGE, + PAGES_MODULE_KINDS } from "../utils/constants"; import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions"; @@ -143,7 +145,16 @@ export const resetPageTemplateForm = () => (dispatch) => { const normalizeEntity = (entity) => { const normalizedEntity = { ...entity }; - normalizedEntity.modules = []; + normalizedEntity.modules = entity.modules.map((module) => { + const normalizedModule = { ...module }; + + if (module.kind === PAGES_MODULE_KINDS.MEDIA && module.upload_deadline) { + normalizedModule.upload_deadline = moment(module.upload_deadline).unix(); + } + delete normalizedModule._tempId; + + return normalizedModule; + }); return normalizedEntity; }; diff --git a/src/components/mui/formik-inputs/mui-formik-datepicker.js b/src/components/mui/formik-inputs/mui-formik-datepicker.js index b950da55a..c27b5c702 100644 --- a/src/components/mui/formik-inputs/mui-formik-datepicker.js +++ b/src/components/mui/formik-inputs/mui-formik-datepicker.js @@ -5,7 +5,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; import { useField } from "formik"; -const MuiFormikDatepicker = ({ name, label }) => { +const MuiFormikDatepicker = ({ name, label, ...props }) => { const [field, meta, helpers] = useField(name); return ( @@ -18,10 +18,12 @@ const MuiFormikDatepicker = ({ name, label }) => { label, error: meta.touched && Boolean(meta.error), helperText: meta.touched && meta.error, - fullWidth: true, - margin: "normal" + fullWidth: true } }} + margin="normal" + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} /> ); diff --git a/src/components/mui/formik-inputs/mui-formik-radio-group.js b/src/components/mui/formik-inputs/mui-formik-radio-group.js index 01200434f..79cb82327 100644 --- a/src/components/mui/formik-inputs/mui-formik-radio-group.js +++ b/src/components/mui/formik-inputs/mui-formik-radio-group.js @@ -10,13 +10,19 @@ import { } from "@mui/material"; import { useField } from "formik"; -const MuiFormikRadioGroup = ({ name, label, options, ...props }) => { +const MuiFormikRadioGroup = ({ + name, + label, + margin = "normal", + options, + ...props +}) => { const [field, meta] = useField({ name }); return ( {label && {label}} @@ -56,6 +62,7 @@ const MuiFormikRadioGroup = ({ name, label, options, ...props }) => { MuiFormikRadioGroup.propTypes = { name: PropTypes.string.isRequired, label: PropTypes.string, + margin: PropTypes.string, options: PropTypes.array.isRequired }; diff --git a/src/components/mui/formik-inputs/mui-formik-select.js b/src/components/mui/formik-inputs/mui-formik-select.js index 226737a18..a50db2fa8 100644 --- a/src/components/mui/formik-inputs/mui-formik-select.js +++ b/src/components/mui/formik-inputs/mui-formik-select.js @@ -5,12 +5,13 @@ import { FormHelperText, FormControl, InputAdornment, - IconButton + IconButton, + InputLabel } from "@mui/material"; import ClearIcon from "@mui/icons-material/Clear"; import { useField } from "formik"; -const MuiFormikSelect = ({ name, children, isClearable, ...rest }) => { +const MuiFormikSelect = ({ name, label, children, isClearable, ...rest }) => { const [field, meta, helpers] = useField(name); const handleClear = (ev) => { @@ -18,13 +19,24 @@ const MuiFormikSelect = ({ name, children, isClearable, ...rest }) => { helpers.setValue(""); }; + const hasValue = + field.value !== "" && field.value !== undefined && field.value !== null; + return ( + {label && ( + + {label} + + )} diff --git a/src/components/mui/formik-inputs/mui-formik-upload.js b/src/components/mui/formik-inputs/mui-formik-upload.js index fd9f0c13a..7304af806 100644 --- a/src/components/mui/formik-inputs/mui-formik-upload.js +++ b/src/components/mui/formik-inputs/mui-formik-upload.js @@ -10,45 +10,94 @@ import { MAX_INVENTORY_IMAGES_UPLOAD_QTY } from "../../../utils/constants"; -const MuiFormikUpload = ({ id, name, onImageDeleted }) => { +const MuiFormikUpload = ({ id, name, onImageDeleted, singleFile = false }) => { const [field, meta, helpers] = useField(name); console.log("images: ", field.value); const mediaType = { max_size: MAX_INVENTORY_IMAGE_UPLOAD_SIZE, - max_uploads_qty: MAX_INVENTORY_IMAGES_UPLOAD_QTY, + max_uploads_qty: singleFile ? 1 : MAX_INVENTORY_IMAGES_UPLOAD_QTY, type: { allowed_extensions: ALLOWED_INVENTORY_IMAGE_FORMATS } }; - const getInputValue = () => - field.value?.length > 0 + const getInputValue = () => { + if (singleFile) { + if (!field.value || Object.keys(field.value).length === 0) { + return []; + } + return [ + { + ...field.value, + filename: + field.value.file_name ?? + field.value.filename ?? + field.value.file_path + } + ]; + } + return field.value?.length > 0 ? field.value.map((img) => ({ ...img, filename: img.filename ?? img.file_path ?? img.file_url })) : []; + }; + + const buildFileObject = (response) => { + const file = {}; + + if (response.id !== undefined) file.id = response.id; + if (response.name) file.file_name = response.name; + if (response.md5) file.md5 = response.md5; + if (response.mime_type) file.mime_type = response.mime_type; + if (response.source_bucket) file.bucket = response.source_bucket; + if (response.path && response.name) + file.file_path = `${response.path}${response.name}`; + + return file; + }; const handleUploadComplete = (response) => { if (response) { - const image = { - file_path: `${response.path}${response.name}`, - filename: response.name - }; - helpers.setValue([...field.value, image]); + console.log("CHJECK RESPONSE", response); + const image = buildFileObject(response); + if (singleFile) { + helpers.setValue(image); + } else { + helpers.setValue([...(field.value || []), image]); + } helpers.setTouched(true); } }; const handleRemove = (imageFile) => { - const updated = field.value.filter((i) => i.filename !== imageFile.name); - helpers.setValue(updated); + if (singleFile) { + if (onImageDeleted && field.value?.id) { + onImageDeleted(field.value.id); + } + helpers.setValue(null); + } else { + const updated = (field.value || []).filter( + (i) => i.filename !== imageFile.name + ); + helpers.setValue(updated); + if (onImageDeleted) { + onImageDeleted(imageFile.id); + } + } + }; - if (onImageDeleted) { - onImageDeleted(imageFile.id); + const canAddMore = () => { + if (singleFile) { + return !field.value || Object.keys(field.value).length === 0; } + return ( + mediaType.is_editable || + (field.value?.length || 0) < mediaType.max_uploads_qty + ); }; return ( @@ -66,10 +115,7 @@ const MuiFormikUpload = ({ id, name, onImageDeleted }) => { postUrl={`${window.FILE_UPLOAD_API_BASE_URL}/api/v1/files/upload`} djsConfig={{ withCredentials: true }} maxFiles={mediaType.max_uploads_qty} - canAdd={ - mediaType.is_editable || - (field.value?.length || 0) < mediaType.max_uploads_qty - } + canAdd={canAddMore()} parallelChunkUploads /> > @@ -77,7 +123,8 @@ const MuiFormikUpload = ({ id, name, onImageDeleted }) => { }; MuiFormikUpload.propTypes = { - name: PropTypes.string.isRequired + name: PropTypes.string.isRequired, + singleFile: PropTypes.bool }; export default MuiFormikUpload; diff --git a/src/i18n/en.json b/src/i18n/en.json index 506fed838..90edb6db1 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -3873,6 +3873,8 @@ "alert_info": "You can create or archive Pages from the list. To edit a Page click on the item's Edit botton.", "code": "Code", "name": "Name", + "sponsorship": "Always apply to", + "all_tiers": "All Tiers", "info_mod": "Info Mod", "upload_mod": "Upload Mod", "download_mod": "Download Mod", @@ -3885,7 +3887,8 @@ "add_form_template": "New Form", "add_using_global_template": "Using Global Template", "placeholders": { - "search": "Search" + "search": "Search", + "sponsorship_type": "Always apply to" }, "page_crud": { "title": "Create New Page", @@ -3895,7 +3898,21 @@ "no_modules": "No modules added yet.", "save": "Save Page", "page_saved": "Page saved successfully.", - "page_created": "Page created successfully." + "page_created": "Page created successfully.", + "info_module": "Info Module", + "info_content": "Info Content", + "document_module": "Document Download Module", + "document_name": "Document Name", + "description": "Description", + "external_url": "External URL", + "upload_file": "Upload File", + "text_input": "Text input", + "media_module": "Media Request Module", + "name": "Name", + "upload_deadline": "Upload Deadline", + "max_file_size": "Max File Size (MB)", + "allowed_formats": "Allowed Formats", + "module_remove_warning": "Are you sure you want to delete {name}" } } } diff --git a/src/pages/sponsors-global/page-templates/page-template-module-form.test.js b/src/pages/sponsors-global/page-templates/page-template-module-form.test.js new file mode 100644 index 000000000..eba5e2eea --- /dev/null +++ b/src/pages/sponsors-global/page-templates/page-template-module-form.test.js @@ -0,0 +1,497 @@ +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form, useFormikContext } from "formik"; +import "@testing-library/jest-dom"; +import PageModules from "./page-template-modules-form"; +import showConfirmDialog from "../../../components/mui/showConfirmDialog"; +import { + PAGES_MODULE_KINDS, + PAGE_MODULES_MEDIA_TYPES +} from "../../../utils/constants"; + +// Mocks +jest.mock("../../../components/mui/showConfirmDialog", () => jest.fn()); + +jest.mock( + "../../../components/inputs/formik-text-editor", + () => + function MockFormikTextEditor({ name }) { + return ; + } +); + +jest.mock( + "../../../components/mui/formik-inputs/mui-formik-upload", + () => + function MockMuiFormikUpload({ name }) { + return Upload; + } +); + +jest.mock( + "../../../components/mui/formik-inputs/mui-formik-textfield", + () => + function MockMuiFormikTextField({ name }) { + return ; + } +); + +jest.mock( + "../../../components/mui/formik-inputs/mui-formik-select", + () => + function MockMuiFormikSelect({ name, children }) { + return {children}; + } +); + +jest.mock( + "../../../components/mui/formik-inputs/mui-formik-datepicker", + () => + function MockMuiFormikDatepicker({ name }) { + return ; + } +); + +jest.mock( + "../../../components/mui/formik-inputs/mui-formik-radio-group", + () => + function MockMuiFormikRadioGroup({ name }) { + return ; + } +); + +// Mock DragAndDropList que captura onReorder +let capturedOnReorder = null; +jest.mock( + "../../../components/mui/dnd-list", + () => + function MockDragAndDropList({ items, renderItem, onReorder }) { + capturedOnReorder = onReorder; + return ( + + {items.map((item, index) => ( + + {renderItem(item, index)} + + ))} + + ); + } +); + +// Helper function to render the component with Formik +const renderWithFormik = (initialValues = { modules: [] }) => + render( + + + + + + ); + +describe("PageModules", () => { + const createModule = (kind, order, id) => ({ + _tempId: `temp-${id}`, + kind, + custom_order: order, + name: `Module ${id}`, + content: "", + description: "", + ...(kind === PAGES_MODULE_KINDS.MEDIA && { + type: PAGE_MODULES_MEDIA_TYPES.FILE, + upload_deadline: null, + max_file_size: 100, + file_type_id: 1 + }), + ...(kind === PAGES_MODULE_KINDS.DOCUMENT && { + external_url: "", + file: null + }) + }); + + beforeEach(() => { + jest.clearAllMocks(); + capturedOnReorder = null; + }); + + describe("Rendering", () => { + test("renders empty state message when no modules exist", () => { + renderWithFormik({ modules: [] }); + + expect( + screen.getByText("page_template_list.page_crud.no_modules") + ).toBeInTheDocument(); + }); + + test("renders DragAndDropList when modules exist", () => { + const modules = [createModule(PAGES_MODULE_KINDS.INFO, 0, 1)]; + renderWithFormik({ modules }); + + expect(screen.getByTestId("dnd-list")).toBeInTheDocument(); + expect( + screen.queryByText("page_template_list.page_crud.no_modules") + ).not.toBeInTheDocument(); + }); + + test("renders correct number of modules", () => { + const modules = [ + createModule(PAGES_MODULE_KINDS.INFO, 0, 1), + createModule(PAGES_MODULE_KINDS.DOCUMENT, 1, 2), + createModule(PAGES_MODULE_KINDS.MEDIA, 2, 3) + ]; + + renderWithFormik({ modules }); + + expect(screen.getByTestId("dnd-item-0")).toBeInTheDocument(); + expect(screen.getByTestId("dnd-item-1")).toBeInTheDocument(); + expect(screen.getByTestId("dnd-item-2")).toBeInTheDocument(); + }); + }); + + describe("Module ordering", () => { + test("renders modules in the order they appear in the array", () => { + const modules = [ + createModule(PAGES_MODULE_KINDS.INFO, 0, 1), + createModule(PAGES_MODULE_KINDS.DOCUMENT, 1, 2), + createModule(PAGES_MODULE_KINDS.MEDIA, 2, 3) + ]; + + renderWithFormik({ modules }); + + expect( + screen.getByTestId("text-editor-modules[0].content") + ).toBeInTheDocument(); + expect(screen.getByTestId("upload-modules[1].file")).toBeInTheDocument(); + expect( + screen.getByTestId("datepicker-modules[2].upload_deadline") + ).toBeInTheDocument(); + }); + + test("maintains custom_order values after rendering", () => { + const TestWrapper = () => { + const { values } = useFormikContext(); + return ( + <> + + + {values.modules.map((m, i) => ( + // eslint-disable-next-line + + {m.custom_order} + + ))} + + > + ); + }; + + const modules = [ + createModule(PAGES_MODULE_KINDS.INFO, 0, 1), + createModule(PAGES_MODULE_KINDS.DOCUMENT, 1, 2), + createModule(PAGES_MODULE_KINDS.MEDIA, 2, 3) + ]; + + render( + + + + + + ); + + expect(screen.getByTestId("order-0")).toHaveTextContent("0"); + expect(screen.getByTestId("order-1")).toHaveTextContent("1"); + expect(screen.getByTestId("order-2")).toHaveTextContent("2"); + }); + }); + + describe("Drag and drop reordering", () => { + test("updates modules order when onReorder is called", async () => { + const TestWrapper = () => { + const { values } = useFormikContext(); + return ( + <> + + + {values.modules.map((m) => m._tempId).join(",")} + + > + ); + }; + + const modules = [ + createModule(PAGES_MODULE_KINDS.INFO, 0, 1), + createModule(PAGES_MODULE_KINDS.DOCUMENT, 1, 2), + createModule(PAGES_MODULE_KINDS.MEDIA, 2, 3) + ]; + + render( + + + + + + ); + + expect(screen.getByTestId("module-ids")).toHaveTextContent( + "temp-1,temp-2,temp-3" + ); + + // move first module to the end + const reorderedModules = [modules[1], modules[2], modules[0]]; + capturedOnReorder(reorderedModules); + + await waitFor(() => { + expect(screen.getByTestId("module-ids")).toHaveTextContent( + "temp-2,temp-3,temp-1" + ); + }); + }); + + test("updates field indices after reordering", async () => { + const TestWrapper = () => { + const { values } = useFormikContext(); + return ( + <> + + {values.modules[0]?.kind} + > + ); + }; + + const modules = [ + createModule(PAGES_MODULE_KINDS.INFO, 0, 1), + createModule(PAGES_MODULE_KINDS.DOCUMENT, 1, 2) + ]; + + render( + + + + + + ); + + expect(screen.getByTestId("first-module-kind")).toHaveTextContent( + PAGES_MODULE_KINDS.INFO + ); + + // invert order + const reorderedModules = [modules[1], modules[0]]; + capturedOnReorder(reorderedModules); + + await waitFor(() => { + expect(screen.getByTestId("first-module-kind")).toHaveTextContent( + PAGES_MODULE_KINDS.DOCUMENT + ); + }); + }); + }); + + describe("Accordion expand/collapse", () => { + test("accordion is expanded by default", () => { + const modules = [createModule(PAGES_MODULE_KINDS.INFO, 0, 1)]; + renderWithFormik({ modules }); + + // content should be visible + expect( + screen.getByTestId("text-editor-modules[0].content") + ).toBeVisible(); + }); + + test("collapses accordion when clicking on summary", async () => { + const modules = [createModule(PAGES_MODULE_KINDS.INFO, 0, 1)]; + renderWithFormik({ modules }); + + const expandIcon = screen.getByTestId("ExpandMoreIcon"); + const accordionSummary = expandIcon.closest(".MuiAccordionSummary-root"); + + await userEvent.click(accordionSummary); + + await waitFor(() => { + expect( + screen.getByTestId("text-editor-modules[0].content") + ).not.toBeVisible(); + }); + }); + + test("expands collapsed accordion when clicking on summary", async () => { + const modules = [createModule(PAGES_MODULE_KINDS.INFO, 0, 1)]; + renderWithFormik({ modules }); + + const expandIcon = screen.getByTestId("ExpandMoreIcon"); + const accordionSummary = expandIcon.closest(".MuiAccordionSummary-root"); + + // close + await userEvent.click(accordionSummary); + + await waitFor(() => { + expect( + screen.getByTestId("text-editor-modules[0].content") + ).not.toBeVisible(); + }); + + // expand + await userEvent.click(accordionSummary); + + await waitFor(() => { + expect( + screen.getByTestId("text-editor-modules[0].content") + ).toBeVisible(); + }); + }); + + test("each accordion operates independently", async () => { + const modules = [ + createModule(PAGES_MODULE_KINDS.INFO, 0, 1), + createModule(PAGES_MODULE_KINDS.DOCUMENT, 1, 2) + ]; + renderWithFormik({ modules }); + + const expandIcons = screen.getAllByTestId("ExpandMoreIcon"); + const firstAccordionSummary = expandIcons[0].closest( + ".MuiAccordionSummary-root" + ); + + // close first module + await userEvent.click(firstAccordionSummary); + + await waitFor(() => { + // first module should be closed + expect( + screen.getByTestId("text-editor-modules[0].content") + ).not.toBeVisible(); + // second module should be expanded + expect(screen.getByTestId("upload-modules[1].file")).toBeVisible(); + }); + }); + }); + + describe("handleDeleteModule", () => { + test("shows confirmation dialog when delete button is clicked", async () => { + showConfirmDialog.mockResolvedValue(false); + + const modules = [createModule(PAGES_MODULE_KINDS.INFO, 0, 1)]; + renderWithFormik({ modules }); + + const deleteButton = screen.getByTestId("DeleteIcon").closest("button"); + await userEvent.click(deleteButton); + + expect(showConfirmDialog).toHaveBeenCalledWith( + expect.objectContaining({ + title: "general.are_you_sure", + type: "warning", + showCancelButton: true + }) + ); + }); + + test("removes module from list when confirmed", async () => { + showConfirmDialog.mockResolvedValue(true); + + const TestWrapper = () => { + const { values } = useFormikContext(); + return ( + <> + + {values.modules.length} + > + ); + }; + + const modules = [ + createModule(PAGES_MODULE_KINDS.INFO, 0, 1), + createModule(PAGES_MODULE_KINDS.DOCUMENT, 1, 2) + ]; + + render( + + + + + + ); + + expect(screen.getByTestId("module-count")).toHaveTextContent("2"); + + const deleteButtons = screen.getAllByTestId("DeleteIcon"); + await userEvent.click(deleteButtons[0].closest("button")); + + await waitFor(() => { + expect(screen.getByTestId("module-count")).toHaveTextContent("1"); + }); + }); + + test("does not remove module when cancelled", async () => { + showConfirmDialog.mockResolvedValue(false); + + const TestWrapper = () => { + const { values } = useFormikContext(); + return ( + <> + + {values.modules.length} + > + ); + }; + + const modules = [createModule(PAGES_MODULE_KINDS.INFO, 0, 1)]; + + render( + + + + + + ); + + const deleteButton = screen.getByTestId("DeleteIcon").closest("button"); + await userEvent.click(deleteButton); + + await waitFor(() => { + expect(screen.getByTestId("module-count")).toHaveTextContent("1"); + }); + }); + + test("removes correct module when deleting from middle of list", async () => { + showConfirmDialog.mockResolvedValue(true); + + const TestWrapper = () => { + const { values } = useFormikContext(); + return ( + <> + + + {values.modules.map((m) => m._tempId).join(",")} + + > + ); + }; + + const modules = [ + createModule(PAGES_MODULE_KINDS.INFO, 0, 1), + createModule(PAGES_MODULE_KINDS.DOCUMENT, 1, 2), + createModule(PAGES_MODULE_KINDS.MEDIA, 2, 3) + ]; + + render( + + + + + + ); + + // deletes middle module + const deleteButtons = screen.getAllByTestId("DeleteIcon"); + await userEvent.click(deleteButtons[1].closest("button")); + + await waitFor(() => { + expect(screen.getByTestId("module-ids")).toHaveTextContent( + "temp-1,temp-3" + ); + }); + }); + }); +}); diff --git a/src/pages/sponsors-global/page-templates/page-template-modules-form.js b/src/pages/sponsors-global/page-templates/page-template-modules-form.js new file mode 100644 index 000000000..0c34cf946 --- /dev/null +++ b/src/pages/sponsors-global/page-templates/page-template-modules-form.js @@ -0,0 +1,349 @@ +import React, { useRef, useEffect } from "react"; +import PropTypes from "prop-types"; +import T from "i18n-react/dist/i18n-react"; +import { useFormikContext, getIn } from "formik"; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Box, + Grid2, + IconButton, + Typography, + MenuItem, + InputLabel, + Divider +} from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import DeleteIcon from "@mui/icons-material/Delete"; +import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore"; +import MuiFormikTextField from "../../../components/mui/formik-inputs/mui-formik-textfield"; +import MuiFormikSelect from "../../../components/mui/formik-inputs/mui-formik-select"; +import MuiFormikDatepicker from "../../../components/mui/formik-inputs/mui-formik-datepicker"; +import MuiFormikRadioGroup from "../../../components/mui/formik-inputs/mui-formik-radio-group"; +import DragAndDropList from "../../../components/mui/dnd-list"; +import showConfirmDialog from "../../../components/mui/showConfirmDialog"; +import { + PAGES_MODULE_KINDS, + PAGE_MODULES_MEDIA_TYPES +} from "../../../utils/constants"; +import FormikTextEditor from "../../../components/inputs/formik-text-editor"; +import MuiFormikUpload from "../../../components/mui/formik-inputs/mui-formik-upload"; + +const InfoModule = ({ baseName, index }) => { + const buildFieldName = (field) => `${baseName}[${index}].${field}`; + + return ( + + + {T.translate("page_template_list.page_crud.info_content")} + + + + + + + + ); +}; + +const DocumentDownloadModule = ({ baseName, index }) => { + const buildFieldName = (field) => `${baseName}[${index}].${field}`; + + return ( + + + {T.translate("page_template_list.page_crud.document_name")} + + + + + + {T.translate("page_template_list.page_crud.description")} + + + + + + {T.translate("page_template_list.page_crud.external_url")} + + + + + + + + + ); +}; + +const MediaRequestModule = ({ baseName, index }) => { + const { values } = useFormikContext(); + const buildFieldName = (field) => `${baseName}[${index}].${field}`; + + const mediaType = + getIn(values, buildFieldName("type")) || PAGE_MODULES_MEDIA_TYPES.FILE; + + const mediaTypeOptions = [ + { + value: PAGE_MODULES_MEDIA_TYPES.FILE, + label: T.translate("page_template_list.page_crud.upload_file") + }, + { + value: PAGE_MODULES_MEDIA_TYPES.TEXT, + label: T.translate("page_template_list.page_crud.text_input") + } + ]; + + return ( + + + + + + + + + + {T.translate("page_template_list.page_crud.name")} + + + + + + {T.translate("page_template_list.page_crud.upload_deadline")} + + + + + {mediaType === PAGE_MODULES_MEDIA_TYPES.FILE && ( + <> + + + {T.translate("page_template_list.page_crud.max_file_size")} + + + + + + {T.translate("page_template_list.page_crud.allowed_formats")} + + + PDF + + + > + )} + + + + {T.translate("page_template_list.page_crud.description")} + + + + + ); +}; + +const PageModules = ({ name = "modules" }) => { + const { values, setFieldValue } = useFormikContext(); + const modules = getIn(values, name) || []; + + const bottomRef = useRef(null); + const prevModulesLength = useRef(modules.length); + + // auto-scroll to new module + useEffect(() => { + if (modules.length > prevModulesLength.current) { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + } + prevModulesLength.current = modules.length; + }, [modules.length]); + + const getModuleTitle = (kind) => { + switch (kind) { + case PAGES_MODULE_KINDS.INFO: + return T.translate("page_template_list.page_crud.info_module"); + case PAGES_MODULE_KINDS.DOCUMENT: + return T.translate("page_template_list.page_crud.document_module"); + case PAGES_MODULE_KINDS.MEDIA: + return T.translate("page_template_list.page_crud.media_module"); + default: + return "Module"; + } + }; + + const handleDeleteModule = async (index, module) => { + const moduleName = getModuleTitle(module.kind); + + const isConfirmed = await showConfirmDialog({ + title: T.translate("general.are_you_sure"), + text: T.translate("page_template_list.page_crud.module_remove_warning", { + name: moduleName + }), + type: "warning", + showCancelButton: true, + confirmButtonColor: "#DD6B55", + confirmButtonText: T.translate("general.yes_delete") + }); + + if (isConfirmed) { + const updated = modules.filter((_, i) => i !== index); + setFieldValue(name, updated); + } + }; + + const handleReorderModules = (newModules) => { + setFieldValue(name, newModules); + }; + + const renderModuleFields = (module, index) => { + switch (module.kind) { + case PAGES_MODULE_KINDS.INFO: + return ; + case PAGES_MODULE_KINDS.DOCUMENT: + return ; + case PAGES_MODULE_KINDS.MEDIA: + return ; + default: + return null; + } + }; + + const renderModule = (module, index) => ( + + } + sx={{ + backgroundColor: "#2196F31F", + flexDirection: "row-reverse", + "& .MuiAccordionSummary-expandIconWrapper": { + marginRight: 1, + marginLeft: 0 + } + }} + > + + {getModuleTitle(module.kind)} + + e.stopPropagation()} + > + + handleDeleteModule(index, module)} + > + + + + + + + {renderModuleFields(module, index)} + + + ); + + return ( + + {modules.length === 0 ? ( + + {T.translate("page_template_list.page_crud.no_modules")} + + ) : ( + + )} + {/* mock element to scroll to latest module */} + + + ); +}; + +PageModules.propTypes = { + name: PropTypes.string +}; + +export default PageModules; diff --git a/src/pages/sponsors-global/page-templates/page-template-popup.js b/src/pages/sponsors-global/page-templates/page-template-popup.js index 6fc8f89c6..7c9000358 100644 --- a/src/pages/sponsors-global/page-templates/page-template-popup.js +++ b/src/pages/sponsors-global/page-templates/page-template-popup.js @@ -19,27 +19,60 @@ import CloseIcon from "@mui/icons-material/Close"; import { FormikProvider, useFormik } from "formik"; import * as yup from "yup"; import MuiFormikTextField from "../../../components/mui/formik-inputs/mui-formik-textfield"; +import PageModules from "./page-template-modules-form"; +import { + PAGES_MODULE_KINDS, + PAGE_MODULES_MEDIA_TYPES +} from "../../../utils/constants"; const PageTemplatePopup = ({ pageTemplate, open, onClose, onSave }) => { const handleClose = () => { onClose(); }; + const addModule = (moduleData) => { + const modules = formik.values.modules || []; + const newModule = { + ...moduleData, + _tempId: `temp-${Date.now()}`, + custom_order: modules.length + }; + formik.setFieldValue("modules", [...modules, newModule]); + }; + const handleAddInfo = () => { - console.log("ADD INFO"); + addModule({ + kind: PAGES_MODULE_KINDS.INFO, + content: "" + }); }; const handleAddDocument = () => { - console.log("ADD DOCUMENT"); + addModule({ + kind: PAGES_MODULE_KINDS.DOCUMENT, + name: "", + description: "", + external_url: "", + file: {} + }); }; const handleAddMedia = () => { - console.log("ADD MEDIA"); + addModule({ + kind: PAGES_MODULE_KINDS.MEDIA, + type: PAGE_MODULES_MEDIA_TYPES.FILE, + name: "", + description: "", + upload_deadline: null, + max_file_size: 0, + file_type_id: 0 + }); }; const formik = useFormik({ initialValues: { - ...pageTemplate + ...pageTemplate, + modules: pageTemplate?.modules || [] }, validationSchema: yup.object().shape({ code: yup.string().required(T.translate("validation.required")), @@ -47,7 +80,11 @@ const PageTemplatePopup = ({ pageTemplate, open, onClose, onSave }) => { }), enableReinitialize: true, onSubmit: (values) => { - onSave(values); + const modulesWithOrder = values.modules.map((m, idx) => ({ + ...m, + custom_order: idx + })); + onSave({ ...values, modules: modulesWithOrder }); } }); @@ -120,14 +157,9 @@ const PageTemplatePopup = ({ pageTemplate, open, onClose, onSave }) => { - - {T.translate("page_template_list.page_crud.no_modules")} - + + + diff --git a/src/utils/constants.js b/src/utils/constants.js index e273e38ce..2fbf04e40 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -224,3 +224,14 @@ export const ROOM_OCCUPANCY_OPTIONS = [ "FULL", "OVERFLOW" ]; + +export const PAGES_MODULE_KINDS = { + INFO: "Info", + DOCUMENT: "Document", + MEDIA: "Media" +}; + +export const PAGE_MODULES_MEDIA_TYPES = { + FILE: "file", + TEXT: "text" +};