diff --git a/src/actions/sponsor-pages-actions.js b/src/actions/sponsor-pages-actions.js new file mode 100644 index 000000000..e93e0042c --- /dev/null +++ b/src/actions/sponsor-pages-actions.js @@ -0,0 +1,134 @@ +/** + * Copyright 2018 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 { + authErrorHandler, + createAction, + getRequest, + postRequest, + startLoading, + stopLoading +} from "openstack-uicore-foundation/lib/utils/actions"; +import T from "i18n-react/dist/i18n-react"; +import { escapeFilterValue, getAccessTokenSafely } from "../utils/methods"; +import { + DEFAULT_CURRENT_PAGE, + DEFAULT_ORDER_DIR, + DEFAULT_PER_PAGE +} from "../utils/constants"; +import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions"; + +export const REQUEST_SPONSOR_PAGES = "REQUEST_SPONSOR_PAGES"; +export const RECEIVE_SPONSOR_PAGES = "RECEIVE_SPONSOR_PAGES"; + +export const GLOBAL_PAGE_CLONED = "GLOBAL_PAGE_CLONED"; + +export const getSponsorPages = + ( + term = "", + page = DEFAULT_CURRENT_PAGE, + perPage = DEFAULT_PER_PAGE, + order = "id", + orderDir = DEFAULT_ORDER_DIR, + hideArchived = false, + sponsorshipTypesId = [] + ) => + async (dispatch, getState) => { + const { currentSummitState } = getState(); + const { currentSummit } = currentSummitState; + const accessToken = await getAccessTokenSafely(); + const filter = []; + + dispatch(startLoading()); + + if (term) { + const escapedTerm = escapeFilterValue(term); + filter.push(`name=@${escapedTerm},code=@${escapedTerm}`); + } + + const params = { + page, + per_page: perPage, + access_token: accessToken, + expand: "sponsorship_types" + }; + + if (hideArchived) filter.push("is_archived==0"); + + if (sponsorshipTypesId?.length > 0) { + const formattedSponsorships = sponsorshipTypesId.join("&&"); + filter.push("applies_to_all_tiers==0"); + filter.push(`sponsorship_type_id_not_in==${formattedSponsorships}`); + } + + if (filter.length > 0) { + params["filter[]"] = filter; + } + + // order + if (order != null && orderDir != null) { + const orderDirSign = orderDir === 1 ? "" : "-"; + params.order = `${orderDirSign}${order}`; + } + + return getRequest( + createAction(REQUEST_SPONSOR_PAGES), + createAction(RECEIVE_SPONSOR_PAGES), + `${window.SPONSOR_PAGES_API_URL}/api/v1/summits/${currentSummit.id}/show-pages`, + authErrorHandler, + { order, orderDir, page, term, hideArchived } + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); + }; + +export const cloneGlobalPage = + (pagesIds, sponsorIds, allSponsors) => async (dispatch, getState) => { + const { currentSummitState } = getState(); + const accessToken = await getAccessTokenSafely(); + const { currentSummit } = currentSummitState; + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + const normalizedEntity = { + page_template_ids: pagesIds, + sponsorship_types: sponsorIds, + apply_to_all_types: allSponsors + }; + + if (allSponsors) { + delete normalizedEntity.sponsorship_types; + } + + return postRequest( + null, + createAction(GLOBAL_PAGE_CLONED), + `${window.SPONSOR_PAGES_API_URL}/api/v1/summits/${currentSummit.id}/show-pages/clone`, + normalizedEntity, + snackbarErrorHandler + )(params)(dispatch) + .then(() => { + dispatch(getSponsorForms()); + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate("sponsor_pages.global_page_popup.success") + }) + ); + }) + .catch(() => {}); // need to catch promise reject + }; diff --git a/src/components/menu/index.js b/src/components/menu/index.js index 83e492498..0689c4f10 100644 --- a/src/components/menu/index.js +++ b/src/components/menu/index.js @@ -244,6 +244,11 @@ const getSummitItems = (summitId) => [ linkUrl: `summits/${summitId}/sponsors/forms`, accessRoute: "admin-sponsors" }, + { + name: "sponsor_pages", + linkUrl: `summits/${summitId}/sponsors/pages`, + accessRoute: "admin-sponsors" + }, { name: "sponsorship_list", linkUrl: `summits/${summitId}/sponsorships`, diff --git a/src/i18n/en.json b/src/i18n/en.json index e6ca66683..0161f0b2d 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -154,6 +154,7 @@ "sponsors": "Sponsors", "sponsor_list": "Sponsor List", "sponsor_forms": "Forms", + "sponsor_pages": "Pages", "sponsorship_list": "Tiers", "sponsor_users": "Users", "sponsors_promocodes": "Promo Codes", @@ -2622,6 +2623,40 @@ "items_added": "Items added successfully." } }, + "sponsor_pages": { + "pages": "Pages", + "alert_info": "Note: These Pages will be visible to all sponsors of the selected level in this program.", + "using_template": "Using Template", + "new_page": "New Page", + "code_column_label": "Code", + "name_column_label": "Name", + "tier_column_label": "Tier", + "info_mod_column_label": "Info Mod", + "upload_mod_column_label": "Upload Mod", + "download_mod_column_label": "Download Mod", + "hide_archived": "Hide archived Pages", + "filter": "Filter", + "sort_by": "Sort By", + "no_sponsors_pages": "No pages found for this search criteria.", + "placeholders": { + "search": "Search..." + }, + "global_page_popup": { + "title": "Add Page Template", + "items_selected": "items selected", + "code": "Code", + "name": "name", + "info_mod": "Info Mod", + "download_mod": "Download Mod", + "upload_mod": "Upload Mod", + "add_selected": "Add Selected Page Template", + "success": "Page created successfully.", + "error": "There was a problem creating the forms, please try again.", + "placeholders": { + "search": "Search..." + } + } + }, "sponsor_users": { "users": "Users", "access_request": "access request", diff --git a/src/layouts/sponsor-layout.js b/src/layouts/sponsor-layout.js index fefd4f28d..85ed8dbb9 100644 --- a/src/layouts/sponsor-layout.js +++ b/src/layouts/sponsor-layout.js @@ -25,6 +25,7 @@ import SponsorSettingsPage from "../pages/sponsor_settings/sponsor-settings-page import SponsorFormsListPage from "../pages/sponsors/sponsor-forms-list-page"; import SponsorFormItemListPage from "../pages/sponsors/sponsor-form-item-list-page"; import SponsorUsersListPage from "../pages/sponsors/sponsor-users-list-page"; +import sponsorPagesListPage from "../pages/sponsors/sponsor-pages-list-page"; const SponsorLayout = ({ match }) => (
@@ -63,6 +64,27 @@ const SponsorLayout = ({ match }) => (
)} /> + ( +
+ + + + +
+ )} + /> { + const [stage, setStage] = useState("pages"); + const [selectedTemplates, setSelectedTemplates] = useState([]); + const dialogSize = stage === "pages" ? "md" : "sm"; + + const handleClose = () => { + setSelectedTemplates([]); + setStage("pages"); + onClose(); + }; + + const handleOnSelectTemplates = (templates) => { + setSelectedTemplates(templates); + setStage("sponsorships"); + }; + + const handleOnSave = (selectedTiers, allTiers) => { + cloneGlobalPage(selectedTemplates, selectedTiers, allTiers).finally(() => { + handleClose(); + }); + }; + + return ( + + {stage === "pages" && ( + + )} + {stage === "sponsorships" && ( + + )} + + ); +}; + +GlobalPagePopup.propTypes = { + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired +}; + +const mapStateToProps = () => ({}); + +export default connect(mapStateToProps, { + cloneGlobalPage +})(GlobalPagePopup); diff --git a/src/pages/sponsors/sponsor-pages-list-page/components/global-page/select-pages-dialog.js b/src/pages/sponsors/sponsor-pages-list-page/components/global-page/select-pages-dialog.js new file mode 100644 index 000000000..2a8e4a2b4 --- /dev/null +++ b/src/pages/sponsors/sponsor-pages-list-page/components/global-page/select-pages-dialog.js @@ -0,0 +1,191 @@ +import React, { useEffect, useState } from "react"; +import T from "i18n-react/dist/i18n-react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { + Box, + Button, + Checkbox, + DialogActions, + DialogContent, + DialogTitle, + Divider, + FormControlLabel, + Grid2, + IconButton, + 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"; + +const SelectPagesDialog = ({ + pageTemplates, + items, + currentPage, + term, + order, + orderDir, + total, + onSave, + onClose, + getPageTemplates +}) => { + const [selectedRows, setSelectedRows] = useState([]); + + useEffect(() => { + getPageTemplates("", 1, DEFAULT_PER_PAGE, "id", 1, true); + }, []); + + const handleSort = (key, dir) => { + getPageTemplates(term, 1, DEFAULT_PER_PAGE, key, dir, true); + }; + + const handleLoadMore = () => { + if (total > items.length) { + getPageTemplates( + term, + currentPage + 1, + DEFAULT_PER_PAGE, + order, + orderDir, + true + ); + } + }; + + const handleClose = () => { + setSelectedRows([]); + onClose(); + }; + + const handleOnCheck = (rowId, checked) => { + if (checked) { + setSelectedRows([...selectedRows, rowId]); + } else { + setSelectedRows(selectedRows.filter((r) => r !== rowId)); + } + }; + + const handleOnSearch = (searchTerm) => { + getPageTemplates(searchTerm, 1, DEFAULT_PER_PAGE, "id", 1, true); + }; + + const handleOnSave = () => { + onSave(selectedRows); + }; + + const columns = [ + { + columnKey: "select", + header: "", + width: 30, + align: "center", + render: (row) => ( + handleOnCheck(row.id, ev.target.checked)} + /> + } + /> + ) + }, + { + columnKey: "code", + header: T.translate("sponsor_pages.global_page_popup.code"), + sortable: true + }, + { + columnKey: "name", + header: T.translate("sponsor_pages.global_page_popup.name"), + sortable: true + }, + { + columnKey: "info_mod", + header: T.translate("sponsor_pages.global_page_popup.info_mod"), + sortable: false + }, + { + columnKey: "download_mod", + header: T.translate("sponsor_pages.global_page_popup.download_mod"), + sortable: false + }, + { + columnKey: "upload_mod", + header: T.translate("sponsor_pages.global_page_popup.upload_mod"), + sortable: false + } + ]; + + return ( + <> + + + {T.translate("sponsor_pages.global_page_popup.title")} + + handleClose()}> + + + + + + + + {selectedRows.length} items selected + + + + + + + {pageTemplates.length > 0 && ( + + + + )} + + + + + + + ); +}; + +SelectPagesDialog.propTypes = { + onClose: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired +}; + +const mapStateToProps = ({ pageTemplateListState }) => ({ + ...pageTemplateListState +}); + +export default connect(mapStateToProps, { + getPageTemplates +})(SelectPagesDialog); diff --git a/src/pages/sponsors/sponsor-pages-list-page/index.js b/src/pages/sponsors/sponsor-pages-list-page/index.js new file mode 100644 index 000000000..ad50767a8 --- /dev/null +++ b/src/pages/sponsors/sponsor-pages-list-page/index.js @@ -0,0 +1,223 @@ +/** + * Copyright 2024 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +import React, { useEffect, useState } from "react"; +import { connect } from "react-redux"; +import T from "i18n-react/dist/i18n-react"; +import { + Box, + Button, + Checkbox, + FormControlLabel, + FormGroup, + Grid2 +} from "@mui/material"; +import AddIcon from "@mui/icons-material/Add"; +import { getSponsorPages } from "../../../actions/sponsor-pages-actions"; +import CustomAlert from "../../../components/mui/custom-alert"; +import MuiTable from "../../../components/mui/table/mui-table"; +import GlobalPagePopup from "./components/global-page/global-page-popup"; + +const SponsorPagesListPage = ({ + sponsorPages, + currentPage, + perPage, + term, + order, + orderDir, + hideArchived, + totalCount, + getSponsorPages, + getSponsorForm +}) => { + const [openPopup, setOpenPopup] = useState(null); + + useEffect(() => { + getSponsorPages(); + }, []); + + const handlePageChange = (page) => { + getSponsorPages(term, page, perPage, order, orderDir, hideArchived); + }; + + const handleSort = (key, dir) => { + getSponsorPages(term, currentPage, perPage, key, dir, hideArchived); + }; + + const handleRowEdit = (row) => { + getSponsorForm(row.id).then(() => { + setOpenPopup("new"); + }); + }; + + const handleRowDelete = (itemId) => { + console.log("DELETE ITEM ID...", itemId); + // deleteSponsorForm(itemId); + }; + + const handleArchiveItem = (item) => console.log("archive ITEM...", item); + // item.is_archived + // ? unarchiveSponsorForm(item.id) + // : archiveSponsorForm(item.id); + + const handleHideArchivedForms = (ev) => { + getSponsorPages( + term, + currentPage, + perPage, + order, + orderDir, + ev.target.checked + ); + }; + + const columns = [ + { + columnKey: "code", + header: T.translate("sponsor_pages.code_column_label"), + sortable: true + }, + { + columnKey: "name", + header: T.translate("sponsor_pages.name_column_label"), + sortable: true + }, + { + columnKey: "tier", + header: T.translate("sponsor_pages.tier_column_label"), + sortable: false + }, + { + columnKey: "info_mod", + header: T.translate("sponsor_pages.info_mod_column_label"), + sortable: false + }, + { + columnKey: "upload_mod", + header: T.translate("sponsor_pages.upload_mod_column_label"), + sortable: false + }, + { + columnKey: "download_mod", + header: T.translate("sponsor_pages.download_mod_column_label"), + sortable: false + } + ]; + + const tableOptions = { + sortCol: order, + sortDir: orderDir, + disableProp: "is_archived" + }; + + return ( +
+

{T.translate("sponsor_pages.pages")}

+ + + + + {totalCount} {T.translate("sponsor_pages.pages")} + + + + + + } + label={T.translate("sponsor_pages.hide_archived")} + /> + + + + + + + + + + + + {sponsorPages.length === 0 && ( +
{T.translate("sponsor_pages.no_sponsors_pages")}
+ )} + + {sponsorPages.length > 0 && ( +
+ +
+ )} + + setOpenPopup(null)} + /> + {/* setOpenPopup(null)} + /> */} +
+ ); +}; + +const mapStateToProps = ({ sponsorPagesListState }) => ({ + ...sponsorPagesListState +}); + +export default connect(mapStateToProps, { + getSponsorPages +})(SponsorPagesListPage); diff --git a/src/reducers/sponsors/sponsor-pages-list-reducer.js b/src/reducers/sponsors/sponsor-pages-list-reducer.js new file mode 100644 index 000000000..c835667b0 --- /dev/null +++ b/src/reducers/sponsors/sponsor-pages-list-reducer.js @@ -0,0 +1,85 @@ +/** + * Copyright 2019 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +import { LOGOUT_USER } from "openstack-uicore-foundation/lib/security/actions"; +import { + RECEIVE_SPONSOR_PAGES, + REQUEST_SPONSOR_PAGES +} from "../../actions/sponsor-pages-actions"; +import { SET_CURRENT_SUMMIT } from "../../actions/summit-actions"; + +const DEFAULT_STATE = { + sponsorPages: [], + term: "", + order: "name", + orderDir: 1, + currentPage: 1, + lastPage: 1, + perPage: 10, + totalCount: 0, + hideArchived: false +}; + +const sponsorPagesListReducer = (state = DEFAULT_STATE, action) => { + const { type, payload } = action; + + switch (type) { + case SET_CURRENT_SUMMIT: + case LOGOUT_USER: { + return DEFAULT_STATE; + } + case REQUEST_SPONSOR_PAGES: { + const { order, orderDir, page, term, hideArchived } = payload; + + return { + ...state, + order, + orderDir, + sponsorPages: [], + currentPage: page, + term, + hideArchived + }; + } + case RECEIVE_SPONSOR_PAGES: { + const { + current_page: currentPage, + total, + last_page: lastPage + } = payload.response; + + const sponsorPages = payload.response.data.map((a) => ({ + id: a.id, + code: a.code, + name: a.name, + tier: a.sponsorship_types.map((s) => s.name).join(", "), + info_mod: a.modules.filter((m) => m.kind === "Info").length, + upload_mod: a.modules.filter((m) => m.kind === "Upload").length, + download_mod: a.modules.filter((m) => m.kind === "Download").length, + is_archived: a.is_archived + })); + + return { + ...state, + sponsorPages, + currentPage, + totalCount: total, + lastPage + }; + } + default: + return state; + } +}; + +export default sponsorPagesListReducer; diff --git a/src/store.js b/src/store.js index b3289558e..ae9386539 100644 --- a/src/store.js +++ b/src/store.js @@ -167,6 +167,7 @@ import sponsorPageFormsListReducer from "./reducers/sponsors/sponsor-page-forms- import sponsorCustomizedFormReducer from "./reducers/sponsors/sponsor-customized-form-reducer.js"; import sponsorPageCartListReducer from "./reducers/sponsors/sponsor-page-cart-list-reducer"; import sponsorCustomizedFormItemsListReducer from "./reducers/sponsors/sponsor-customized-form-items-list-reducer.js"; +import sponsorPagesListReducer from "./reducers/sponsors/sponsor-pages-list-reducer.js"; // default: localStorage if web, AsyncStorage if react-native @@ -250,6 +251,7 @@ const reducers = persistCombineReducers(config, { currentSponsorState: sponsorReducer, sponsorFormsListState: sponsorFormsListReducer, sponsorFormItemsListState: sponsorFormItemsListReducer, + sponsorPagesListState: sponsorPagesListReducer, sponsorUsersListState: sponsorUsersListReducer, sponsorPageFormsListState: sponsorPageFormsListReducer, sponsorPageCartListState: sponsorPageCartListReducer,