From 65a2152398bc9e98bf925dc1a6c317f1acd091ae Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 29 Jan 2026 18:23:11 -0300 Subject: [PATCH 1/2] feat: clone page template --- src/actions/page-template-actions.js | 36 ++++++++++- src/actions/sponsor-pages-actions.js | 3 +- .../select-page-template-dialog/index.js} | 63 ++++++++++++------- src/i18n/en.json | 3 +- .../page-template-clone-popup/index.js | 31 +++++++++ .../page-templates/page-template-list-page.js | 8 ++- .../index.js} | 4 +- .../page-template-document-download-module.js | 4 +- .../modules/page-template-info-module.js | 2 +- .../page-template-media-request-module.js | 12 ++-- .../page-template-module-form.test.js | 22 +++---- .../page-template-modules-form.js | 6 +- .../global-page/global-page-popup.js | 5 +- .../page-template-list-reducer.js | 6 +- 14 files changed, 144 insertions(+), 61 deletions(-) rename src/{pages/sponsors/sponsor-pages-list-page/components/global-page/select-pages-dialog.js => components/select-page-template-dialog/index.js} (74%) create mode 100644 src/pages/sponsors-global/page-templates/page-template-clone-popup/index.js rename src/pages/sponsors-global/page-templates/{page-template-popup.js => page-template-popup/index.js} (98%) rename src/pages/sponsors-global/page-templates/{ => page-template-popup}/modules/page-template-document-download-module.js (89%) rename src/pages/sponsors-global/page-templates/{ => page-template-popup}/modules/page-template-info-module.js (91%) rename src/pages/sponsors-global/page-templates/{ => page-template-popup}/modules/page-template-media-request-module.js (85%) rename src/pages/sponsors-global/page-templates/{ => page-template-popup}/page-template-module-form.test.js (95%) rename src/pages/sponsors-global/page-templates/{ => page-template-popup}/page-template-modules-form.js (96%) diff --git a/src/actions/page-template-actions.js b/src/actions/page-template-actions.js index 9283195ef..1fa0f7fab 100644 --- a/src/actions/page-template-actions.js +++ b/src/actions/page-template-actions.js @@ -32,6 +32,7 @@ import { PAGES_MODULE_KINDS } from "../utils/constants"; import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions"; +import { GLOBAL_PAGE_CLONED } from "./sponsor-pages-actions"; export const ADD_PAGE_TEMPLATE = "ADD_PAGE_TEMPLATE"; export const PAGE_TEMPLATE_ADDED = "PAGE_TEMPLATE_ADDED"; @@ -171,7 +172,7 @@ const normalizeEntity = (entity) => { return normalizedEntity; }; -export const savePageTemplate = (entity) => async (dispatch, getState) => { +export const savePageTemplate = (entity) => async (dispatch) => { const accessToken = await getAccessTokenSafely(); const params = { access_token: accessToken @@ -197,7 +198,7 @@ export const savePageTemplate = (entity) => async (dispatch, getState) => { html: T.translate("page_template_list.page_crud.page_saved") }) ); - getPageTemplates()(dispatch, getState); + getPageTemplates()(dispatch); }) .catch((err) => { console.error(err); @@ -222,7 +223,7 @@ export const savePageTemplate = (entity) => async (dispatch, getState) => { html: T.translate("page_template_list.page_crud.page_created") }) ); - getPageTemplates()(dispatch, getState); + getPageTemplates()(dispatch); }) .catch((err) => { console.error(err); @@ -263,3 +264,32 @@ export const unarchivePageTemplate = (pageTemplateId) => async (dispatch) => { dispatch(stopLoading()); }); }; + +export const clonePageTemplate = (templateId) => async (dispatch) => { + const accessToken = await getAccessTokenSafely(); + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + return postRequest( + null, + createAction(GLOBAL_PAGE_CLONED), + `${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${templateId}/clone`, + {}, + snackbarErrorHandler + )(params)(dispatch) + .then(() => { + getPageTemplates()(dispatch); + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate("page_template_list.clone_success") + }) + ); + }) + .catch(console.error) + .finally(() => dispatch(stopLoading())); +}; diff --git a/src/actions/sponsor-pages-actions.js b/src/actions/sponsor-pages-actions.js index cb7355d1d..e71feb99a 100644 --- a/src/actions/sponsor-pages-actions.js +++ b/src/actions/sponsor-pages-actions.js @@ -21,7 +21,6 @@ import { } from "openstack-uicore-foundation/lib/utils/actions"; import T from "i18n-react/dist/i18n-react"; import { escapeFilterValue, getAccessTokenSafely } from "../utils/methods"; -import { getSponsorForms } from "./sponsor-forms-actions"; import { DEFAULT_CURRENT_PAGE, DEFAULT_ORDER_DIR, @@ -123,7 +122,7 @@ export const cloneGlobalPage = snackbarErrorHandler )(params)(dispatch) .then(() => { - dispatch(getSponsorForms()); + dispatch(getSponsorPages()); dispatch( snackbarSuccessHandler({ title: T.translate("general.success"), diff --git a/src/pages/sponsors/sponsor-pages-list-page/components/global-page/select-pages-dialog.js b/src/components/select-page-template-dialog/index.js similarity index 74% rename from src/pages/sponsors/sponsor-pages-list-page/components/global-page/select-pages-dialog.js rename to src/components/select-page-template-dialog/index.js index 2a8e4a2b4..f52f696bf 100644 --- a/src/pages/sponsors/sponsor-pages-list-page/components/global-page/select-pages-dialog.js +++ b/src/components/select-page-template-dialog/index.js @@ -13,17 +13,17 @@ import { FormControlLabel, Grid2, IconButton, + Radio, Typography } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; -import SearchInput from "../../../../../components/mui/search-input"; -import { getPageTemplates } from "../../../../../actions/page-template-actions"; -import { DEFAULT_PER_PAGE } from "../../../../../utils/constants"; -import MuiInfiniteTable from "../../../../../components/mui/infinite-table"; +import SearchInput from "../mui/search-input"; +import { getPageTemplates } from "../../actions/page-template-actions"; +import { DEFAULT_PER_PAGE } from "../../utils/constants"; +import MuiInfiniteTable from "../mui/infinite-table"; -const SelectPagesDialog = ({ +const SelectPageTemplateDialog = ({ pageTemplates, - items, currentPage, term, order, @@ -31,6 +31,7 @@ const SelectPagesDialog = ({ total, onSave, onClose, + isMulti = false, getPageTemplates }) => { const [selectedRows, setSelectedRows] = useState([]); @@ -44,7 +45,7 @@ const SelectPagesDialog = ({ }; const handleLoadMore = () => { - if (total > items.length) { + if (total > pageTemplates.length) { getPageTemplates( term, currentPage + 1, @@ -62,10 +63,15 @@ const SelectPagesDialog = ({ }; const handleOnCheck = (rowId, checked) => { - if (checked) { - setSelectedRows([...selectedRows, rowId]); + if (isMulti) { + if (checked) { + setSelectedRows([...selectedRows, rowId]); + } else { + setSelectedRows(selectedRows.filter((r) => r !== rowId)); + } } else { - setSelectedRows(selectedRows.filter((r) => r !== rowId)); + // For single selection (radio), always set to the selected row + setSelectedRows(checked ? [rowId] : []); } }; @@ -83,17 +89,28 @@ const SelectPagesDialog = ({ header: "", width: 30, align: "center", - render: (row) => ( - handleOnCheck(row.id, ev.target.checked)} - /> - } - /> - ) + render: (row) => + isMulti ? ( + handleOnCheck(row.id, ev.target.checked)} + /> + } + /> + ) : ( + handleOnCheck(row.id, ev.target.checked)} + /> + } + /> + ) }, { columnKey: "code", @@ -177,7 +194,7 @@ const SelectPagesDialog = ({ ); }; -SelectPagesDialog.propTypes = { +SelectPageTemplateDialog.propTypes = { onClose: PropTypes.func.isRequired, onSave: PropTypes.func.isRequired }; @@ -188,4 +205,4 @@ const mapStateToProps = ({ pageTemplateListState }) => ({ export default connect(mapStateToProps, { getPageTemplates -})(SelectPagesDialog); +})(SelectPageTemplateDialog); diff --git a/src/i18n/en.json b/src/i18n/en.json index abea35a2c..9170f97b4 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -3965,6 +3965,7 @@ "max_file_size": "Max File Size (MB)", "allowed_formats": "Allowed Formats", "module_remove_warning": "Are you sure you want to delete {name}" - } + }, + "clone_success": "Page template cloned successfully." } } diff --git a/src/pages/sponsors-global/page-templates/page-template-clone-popup/index.js b/src/pages/sponsors-global/page-templates/page-template-clone-popup/index.js new file mode 100644 index 000000000..39c2f6bf7 --- /dev/null +++ b/src/pages/sponsors-global/page-templates/page-template-clone-popup/index.js @@ -0,0 +1,31 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { Dialog } from "@mui/material"; +import SelectPageTemplateDialog from "../../../../components/select-page-template-dialog"; +import { clonePageTemplate } from "../../../../actions/page-template-actions"; + +const PageTemplateClonePopup = ({ open, onClose, clonePageTemplate }) => { + const handleOnSave = (template) => { + clonePageTemplate(template).finally(() => { + onClose(); + }); + }; + + return ( + + + + ); +}; + +PageTemplateClonePopup.propTypes = { + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired +}; + +const mapStateToProps = () => ({}); + +export default connect(mapStateToProps, { + clonePageTemplate +})(PageTemplateClonePopup); diff --git a/src/pages/sponsors-global/page-templates/page-template-list-page.js b/src/pages/sponsors-global/page-templates/page-template-list-page.js index 34b09dd59..473b5fc0c 100644 --- a/src/pages/sponsors-global/page-templates/page-template-list-page.js +++ b/src/pages/sponsors-global/page-templates/page-template-list-page.js @@ -35,6 +35,7 @@ import MuiTable from "../../../components/mui/table/mui-table"; import SearchInput from "../../../components/mui/search-input"; import { DEFAULT_CURRENT_PAGE } from "../../../utils/constants"; import PageTemplatePopup from "./page-template-popup"; +import PageTemplateClonePopup from "./page-template-clone-popup"; const PageTemplateListPage = ({ pageTemplates, @@ -52,6 +53,7 @@ const PageTemplateListPage = ({ deletePageTemplate }) => { const [pageTemplateId, setPageTemplateId] = useState(null); + const [openCloneDialog, setOpenCloneDialog] = useState(false); useEffect(() => { getPageTemplates(); @@ -103,7 +105,7 @@ const PageTemplateListPage = ({ }; const handleClonePageTemplate = () => { - console.log("CLONE PAGE"); + setOpenCloneDialog(true); }; const handleSavePageTemplate = (entity) => { @@ -267,6 +269,10 @@ const PageTemplateListPage = ({ onClose={() => setPageTemplateId(null)} onSave={handleSavePageTemplate} /> + setOpenCloneDialog(false)} + /> ); }; diff --git a/src/pages/sponsors-global/page-templates/page-template-popup.js b/src/pages/sponsors-global/page-templates/page-template-popup/index.js similarity index 98% rename from src/pages/sponsors-global/page-templates/page-template-popup.js rename to src/pages/sponsors-global/page-templates/page-template-popup/index.js index 586d76919..4514f8908 100644 --- a/src/pages/sponsors-global/page-templates/page-template-popup.js +++ b/src/pages/sponsors-global/page-templates/page-template-popup/index.js @@ -18,12 +18,12 @@ import AddIcon from "@mui/icons-material/Add"; 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 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"; +} from "../../../../utils/constants"; const PageTemplatePopup = ({ pageTemplate, open, onClose, onSave }) => { const handleClose = () => { diff --git a/src/pages/sponsors-global/page-templates/modules/page-template-document-download-module.js b/src/pages/sponsors-global/page-templates/page-template-popup/modules/page-template-document-download-module.js similarity index 89% rename from src/pages/sponsors-global/page-templates/modules/page-template-document-download-module.js rename to src/pages/sponsors-global/page-templates/page-template-popup/modules/page-template-document-download-module.js index 86c4b25f4..029a92a19 100644 --- a/src/pages/sponsors-global/page-templates/modules/page-template-document-download-module.js +++ b/src/pages/sponsors-global/page-templates/page-template-popup/modules/page-template-document-download-module.js @@ -3,8 +3,8 @@ import PropTypes from "prop-types"; import T from "i18n-react/dist/i18n-react"; import { Grid2, InputLabel } from "@mui/material"; -import MuiFormikUpload from "../../../../components/mui/formik-inputs/mui-formik-upload"; -import MuiFormikTextField from "../../../../components/mui/formik-inputs/mui-formik-textfield"; +import MuiFormikUpload from "../../../../../components/mui/formik-inputs/mui-formik-upload"; +import MuiFormikTextField from "../../../../../components/mui/formik-inputs/mui-formik-textfield"; const DocumentDownloadModule = ({ baseName, index }) => { const buildFieldName = (field) => `${baseName}[${index}].${field}`; diff --git a/src/pages/sponsors-global/page-templates/modules/page-template-info-module.js b/src/pages/sponsors-global/page-templates/page-template-popup/modules/page-template-info-module.js similarity index 91% rename from src/pages/sponsors-global/page-templates/modules/page-template-info-module.js rename to src/pages/sponsors-global/page-templates/page-template-popup/modules/page-template-info-module.js index f67549f29..86a8c0070 100644 --- a/src/pages/sponsors-global/page-templates/modules/page-template-info-module.js +++ b/src/pages/sponsors-global/page-templates/page-template-popup/modules/page-template-info-module.js @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import T from "i18n-react/dist/i18n-react"; import { Grid2, Box, InputLabel } from "@mui/material"; -import FormikTextEditor from "../../../../components/inputs/formik-text-editor"; +import FormikTextEditor from "../../../../../components/inputs/formik-text-editor"; const InfoModule = ({ baseName, index }) => { const buildFieldName = (field) => `${baseName}[${index}].${field}`; diff --git a/src/pages/sponsors-global/page-templates/modules/page-template-media-request-module.js b/src/pages/sponsors-global/page-templates/page-template-popup/modules/page-template-media-request-module.js similarity index 85% rename from src/pages/sponsors-global/page-templates/modules/page-template-media-request-module.js rename to src/pages/sponsors-global/page-templates/page-template-popup/modules/page-template-media-request-module.js index 1b6117eac..24269887a 100644 --- a/src/pages/sponsors-global/page-templates/modules/page-template-media-request-module.js +++ b/src/pages/sponsors-global/page-templates/page-template-popup/modules/page-template-media-request-module.js @@ -3,12 +3,12 @@ import PropTypes from "prop-types"; import T from "i18n-react/dist/i18n-react"; import { useFormikContext, getIn } from "formik"; import { Grid2, Divider, InputLabel } from "@mui/material"; -import MuiFormikTextField from "../../../../components/mui/formik-inputs/mui-formik-textfield"; -import MuiFormikDatepicker from "../../../../components/mui/formik-inputs/mui-formik-datepicker"; -import MuiFormikRadioGroup from "../../../../components/mui/formik-inputs/mui-formik-radio-group"; -import { PAGE_MODULES_MEDIA_TYPES } from "../../../../utils/constants"; -import MuiFormikAsyncAutocomplete from "../../../../components/mui/formik-inputs/mui-formik-async-select"; -import { queryMediaFileTypes } from "../../../../actions/media-file-type-actions"; +import MuiFormikTextField from "../../../../../components/mui/formik-inputs/mui-formik-textfield"; +import MuiFormikDatepicker from "../../../../../components/mui/formik-inputs/mui-formik-datepicker"; +import MuiFormikRadioGroup from "../../../../../components/mui/formik-inputs/mui-formik-radio-group"; +import { PAGE_MODULES_MEDIA_TYPES } from "../../../../../utils/constants"; +import MuiFormikAsyncAutocomplete from "../../../../../components/mui/formik-inputs/mui-formik-async-select"; +import { queryMediaFileTypes } from "../../../../../actions/media-file-type-actions"; const MediaRequestModule = ({ baseName, index }) => { const { values } = useFormikContext(); diff --git a/src/pages/sponsors-global/page-templates/page-template-module-form.test.js b/src/pages/sponsors-global/page-templates/page-template-popup/page-template-module-form.test.js similarity index 95% rename from src/pages/sponsors-global/page-templates/page-template-module-form.test.js rename to src/pages/sponsors-global/page-templates/page-template-popup/page-template-module-form.test.js index 2277e4ff9..2a406b3be 100644 --- a/src/pages/sponsors-global/page-templates/page-template-module-form.test.js +++ b/src/pages/sponsors-global/page-templates/page-template-popup/page-template-module-form.test.js @@ -4,17 +4,17 @@ 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 showConfirmDialog from "../../../../components/mui/showConfirmDialog"; import { PAGES_MODULE_KINDS, PAGE_MODULES_MEDIA_TYPES -} from "../../../utils/constants"; +} from "../../../../utils/constants"; // Mocks -jest.mock("../../../components/mui/showConfirmDialog", () => jest.fn()); +jest.mock("../../../../components/mui/showConfirmDialog", () => jest.fn()); jest.mock( - "../../../components/inputs/formik-text-editor", + "../../../../components/inputs/formik-text-editor", () => function MockFormikTextEditor({ name }) { return