diff --git a/.env.example b/.env.example index b71ae142b..1a059072b 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,8 @@ SPONSOR_USERS_API_SCOPES="show-medata/read show-medata/write access-requests/rea EMAIL_SCOPES="clients/read templates/read templates/write emails/read" FILE_UPLOAD_SCOPES="files/upload" SCOPES="profile openid offline_access ${SPONSOR_USERS_API_SCOPES} ${PURCHASES_API_SCOPES} ${EMAIL_SCOPES} ${FILE_UPLOAD_SCOPES} ${SCOPES_BASE_REALM}/summits/delete-event ${SCOPES_BASE_REALM}/summits/write ${SCOPES_BASE_REALM}/summits/write-event ${SCOPES_BASE_REALM}/summits/read/all ${SCOPES_BASE_REALM}/summits/read ${SCOPES_BASE_REALM}/summits/publish-event ${SCOPES_BASE_REALM}/members/read ${SCOPES_BASE_REALM}/members/read/me ${SCOPES_BASE_REALM}/speakers/write ${SCOPES_BASE_REALM}/attendees/write ${SCOPES_BASE_REALM}/members/write ${SCOPES_BASE_REALM}/organizations/write ${SCOPES_BASE_REALM}/organizations/read ${SCOPES_BASE_REALM}/summits/write-presentation-materials ${SCOPES_BASE_REALM}/summits/registration-orders/update ${SCOPES_BASE_REALM}/summits/registration-orders/delete ${SCOPES_BASE_REALM}/summits/registration-orders/create/offline ${SCOPES_BASE_REALM}/summits/badge-scans/read entity-updates/publish ${SCOPES_BASE_REALM}/audit-logs/read" +SPONSOR_PAGES_API_URL=https://sponsor-pages-api.dev.fnopen.com +SPONSOR_PAGES_SCOPES=page-template/read page-template/write GOOGLE_API_KEY= ALLOWED_USER_GROUPS="super-admins administrators summit-front-end-administrators summit-room-administrators track-chairs-admins sponsors" APP_CLIENT_NAME = "openstack" diff --git a/package.json b/package.json index a22c913bc..48106fb79 100644 --- a/package.json +++ b/package.json @@ -66,8 +66,8 @@ "file-loader": "^6.2.0", "file-saver": "^2.0.2", "final-form": "^4.20.7", - "formik": "^2.4.6", "font-awesome": "^4.7.0", + "formik": "^2.4.6", "fs": "^0.0.2", "graphql-query-builder": "^1.0.7", "history": "^4.7.2", @@ -89,7 +89,7 @@ "moment-duration-format": "^2.3.2", "moment-timezone": "^0.5.33", "node-sass": "^7.0.1", - "openstack-uicore-foundation": "4.2.14", + "openstack-uicore-foundation": "4.2.19", "p-limit": "^6.1.0", "path-browserify": "^1.0.1", "postcss-loader": "^6.2.1", @@ -125,6 +125,7 @@ "redux-thunk": "^2.3.0", "sass-loader": "^12.6.0", "segmented-control": "^0.1.12", + "spark-md5": "^3.0.2", "stream-browserify": "^3.0.0", "style-loader": "^3.3.1", "superagent": "^6.1.0", diff --git a/src/actions/page-template-actions.js b/src/actions/page-template-actions.js new file mode 100644 index 000000000..363230881 --- /dev/null +++ b/src/actions/page-template-actions.js @@ -0,0 +1,242 @@ +/** + * 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 T from "i18n-react/dist/i18n-react"; +import { + getRequest, + putRequest, + postRequest, + deleteRequest, + createAction, + stopLoading, + startLoading, + authErrorHandler, + escapeFilterValue +} from "openstack-uicore-foundation/lib/utils/actions"; +import { 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 ADD_PAGE_TEMPLATE = "ADD_PAGE_TEMPLATE"; +export const PAGE_TEMPLATE_ADDED = "PAGE_TEMPLATE_ADDED"; +export const PAGE_TEMPLATE_DELETED = "PAGE_TEMPLATE_DELETED"; +export const PAGE_TEMPLATE_UPDATED = "PAGE_TEMPLATE_UPDATED"; +export const RECEIVE_PAGE_TEMPLATE = "RECEIVE_PAGE_TEMPLATE"; +export const RECEIVE_PAGE_TEMPLATES = "RECEIVE_PAGE_TEMPLATES"; +export const REQUEST_PAGE_TEMPLATES = "REQUEST_PAGE_TEMPLATES"; +export const RESET_PAGE_TEMPLATE_FORM = "RESET_PAGE_TEMPLATE_FORM"; +export const UPDATE_PAGE_TEMPLATE = "UPDATE_PAGE_TEMPLATE"; +export const PAGE_TEMPLATE_ARCHIVED = "PAGE_TEMPLATE_ARCHIVED"; +export const PAGE_TEMPLATE_UNARCHIVED = "PAGE_TEMPLATE_UNARCHIVED"; + +export const getPageTemplates = + ( + term = null, + page = DEFAULT_CURRENT_PAGE, + perPage = DEFAULT_PER_PAGE, + order = "id", + orderDir = DEFAULT_ORDER_DIR, + hideArchived = false + ) => + async (dispatch) => { + const accessToken = await getAccessTokenSafely(); + const filter = []; + + dispatch(startLoading()); + + if (term) { + const escapedTerm = escapeFilterValue(term); + filter.push(`name=@${escapedTerm},code=@${escapedTerm}`); + } + + const params = { + page, + expand: "modules", + fields: + "id,code,name,modules,is_archived,modules.kind,modules.id,modules.content", + relations: "modules,modules.none", + per_page: perPage, + access_token: accessToken + }; + + if (hideArchived) filter.push("is_archived==0"); + + 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_PAGE_TEMPLATES), + createAction(RECEIVE_PAGE_TEMPLATES), + `${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates`, + authErrorHandler, + { order, orderDir, page, perPage, term, hideArchived } + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); + }; + +export const getPageTemplate = (formTemplateId) => async (dispatch) => { + const accessToken = await getAccessTokenSafely(); + + dispatch(startLoading()); + + const params = { + access_token: accessToken, + expand: "materials,meta_fields,meta_fields.values" + }; + + return getRequest( + null, + createAction(RECEIVE_PAGE_TEMPLATE), + `${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${formTemplateId}`, + authErrorHandler + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); +}; + +export const deletePageTemplate = (formTemplateId) => async (dispatch) => { + const accessToken = await getAccessTokenSafely(); + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + return deleteRequest( + null, + createAction(PAGE_TEMPLATE_DELETED)({ formTemplateId }), + `${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${formTemplateId}`, + null, + authErrorHandler + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); +}; + +export const resetPageTemplateForm = () => (dispatch) => { + dispatch(createAction(RESET_PAGE_TEMPLATE_FORM)({})); +}; + +const normalizeEntity = (entity) => { + const normalizedEntity = { ...entity }; + + normalizedEntity.modules = []; + + return normalizedEntity; +}; + +export const savePageTemplate = (entity) => async (dispatch, getState) => { + const accessToken = await getAccessTokenSafely(); + const params = { + access_token: accessToken + }; + + dispatch(startLoading()); + + const normalizedEntity = normalizeEntity(entity); + + if (entity.id) { + return putRequest( + createAction(UPDATE_PAGE_TEMPLATE), + createAction(PAGE_TEMPLATE_UPDATED), + `${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${entity.id}`, + normalizedEntity, + snackbarErrorHandler, + entity + )(params)(dispatch) + .then(() => { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate("page_template_list.page_crud.page_saved") + }) + ); + getPageTemplates()(dispatch, getState); + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + dispatch(stopLoading()); + }); + } + + return postRequest( + createAction(ADD_PAGE_TEMPLATE), + createAction(PAGE_TEMPLATE_ADDED), + `${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates`, + normalizedEntity, + snackbarErrorHandler, + entity + )(params)(dispatch) + .then(() => { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate("page_template_list.page_crud.page_created") + }) + ); + getPageTemplates()(dispatch, getState); + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + dispatch(stopLoading()); + }); +}; + +/* ************************************** ARCHIVE ************************************** */ + +export const archivePageTemplate = (pageTemplateId) => async (dispatch) => { + const accessToken = await getAccessTokenSafely(); + const params = { access_token: accessToken }; + + return putRequest( + null, + createAction(PAGE_TEMPLATE_ARCHIVED), + `${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${pageTemplateId}/archive`, + null, + snackbarErrorHandler + )(params)(dispatch); +}; + +export const unarchivePageTemplate = (pageTemplateId) => async (dispatch) => { + const accessToken = await getAccessTokenSafely(); + const params = { access_token: accessToken }; + + dispatch(startLoading()); + + return deleteRequest( + null, + createAction(PAGE_TEMPLATE_UNARCHIVED)({ pageTemplateId }), + `${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${pageTemplateId}/archive`, + null, + snackbarErrorHandler + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); +}; diff --git a/src/actions/room-occupancy-actions.js b/src/actions/room-occupancy-actions.js index 5870e89ba..81970faad 100644 --- a/src/actions/room-occupancy-actions.js +++ b/src/actions/room-occupancy-actions.js @@ -3,14 +3,15 @@ import { createAction, deleteRequest, escapeFilterValue, - getCSV, getRequest, putRequest, startLoading, - stopLoading + stopLoading, + getRawCSV, + downloadFileByContent } from "openstack-uicore-foundation/lib/utils/actions"; import moment from "moment-timezone"; -import { getAccessTokenSafely } from "../utils/methods"; +import { getAccessTokenSafely, joinCVSChunks } from "../utils/methods"; import { DEFAULT_CURRENT_PAGE, DEFAULT_ORDER_DIR, @@ -143,11 +144,17 @@ export const getEventsForOccupancyCSV = orderDir = DEFAULT_ORDER_DIR ) => async (dispatch, getState) => { - const { currentSummitState } = getState(); - const accessToken = await getAccessTokenSafely(); + const { currentSummitState, currentRoomOccupancyState } = getState(); + const { totalEvents } = currentRoomOccupancyState; + const csvMIME = "text/csv;charset=utf-8"; + const pageSize = 500; + const totalPages = Math.ceil(totalEvents / pageSize); const { currentSummit } = currentSummitState; const filter = []; const summitTZ = currentSummit.time_zone.name; + const cvsFiles = []; + const endpoint = `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/events/csv`; + const accessToken = await getAccessTokenSafely(); dispatch(startLoading()); @@ -174,7 +181,8 @@ export const getEventsForOccupancyCSV = } const params = { - access_token: accessToken + access_token: accessToken, + per_page: pageSize }; if (filter.length > 0) { @@ -192,13 +200,22 @@ export const getEventsForOccupancyCSV = const filename = `summit-${currentSummit.slug}-rooms-occupancy.csv`; - dispatch( - getCSV( - `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/events/csv`, - params, - filename - ) - ); + for (let i = 1; i <= totalPages; i++) { + cvsFiles.push(getRawCSV(endpoint, { ...params, page: i })); + } + + Promise.all(cvsFiles) + .then((files) => { + if (files.length > 0) { + const cvs = joinCVSChunks(files); + // then simulate the file download + downloadFileByContent(filename, cvs, csvMIME); + } + dispatch(stopLoading()); + }) + .catch(() => { + dispatch(stopLoading()); + }); }; export const getCurrentEventForOccupancy = diff --git a/src/actions/sponsor-actions.js b/src/actions/sponsor-actions.js index 6d5f114d3..c48e8a77f 100644 --- a/src/actions/sponsor-actions.js +++ b/src/actions/sponsor-actions.js @@ -436,7 +436,7 @@ export const removeTierFromSponsor = dispatch(startLoading()); - deleteRequest( + return deleteRequest( null, createAction(SPONSOR_TIER_DELETED)({ sponsorshipId }), `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/sponsorships/${sponsorshipId}`, diff --git a/src/actions/sponsor-forms-actions.js b/src/actions/sponsor-forms-actions.js index 9253b92fc..83bb44467 100644 --- a/src/actions/sponsor-forms-actions.js +++ b/src/actions/sponsor-forms-actions.js @@ -131,7 +131,7 @@ export const getSponsorForms = createAction(RECEIVE_SPONSOR_FORMS), `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/show-forms`, authErrorHandler, - { order, orderDir, page, term, hideArchived } + { order, orderDir, page, perPage, term, hideArchived } )(params)(dispatch).then(() => { dispatch(stopLoading()); }); 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/app.js b/src/app.js index edf4be49d..eb5b5e2b2 100644 --- a/src/app.js +++ b/src/app.js @@ -81,6 +81,7 @@ window.MARKETING_API_BASE_URL = process.env.MARKETING_API_BASE_URL; window.EMAIL_API_BASE_URL = process.env.EMAIL_API_BASE_URL; window.PURCHASES_API_URL = process.env.PURCHASES_API_URL; window.SPONSOR_USERS_API_URL = process.env.SPONSOR_USERS_API_URL; +window.SPONSOR_PAGES_API_URL = process.env.SPONSOR_PAGES_API_URL; window.FILE_UPLOAD_API_BASE_URL = process.env.FILE_UPLOAD_API_BASE_URL; window.SIGNAGE_BASE_URL = process.env.SIGNAGE_BASE_URL; window.INVENTORY_API_BASE_URL = process.env.INVENTORY_API_BASE_URL; diff --git a/src/components/forms/sponsor-general-form/add-extra-question-popup.js b/src/components/forms/sponsor-general-form/add-extra-question-popup.js index 4c0d4fa4f..dea7b6733 100644 --- a/src/components/forms/sponsor-general-form/add-extra-question-popup.js +++ b/src/components/forms/sponsor-general-form/add-extra-question-popup.js @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; -import { FormikProvider, useFormik } from "formik"; +import { FieldArray, FormikProvider, useFormik } from "formik"; import * as yup from "yup"; import PropTypes from "prop-types"; import { @@ -61,16 +61,16 @@ const AddSponsorExtraQuestionPopup = ({ }, validationSchema: yup.object({ id: yup.number(), - name: yup.string().required(), - label: yup.string().required(), - type: yup.string().required(), + name: yup.string().required(T.translate("validation.required")), + label: yup.string().required(T.translate("validation.required")), + type: yup.string().required(T.translate("validation.required")), mandatory: yup.boolean(), placeholder: yup.string(), max_selected_values: yup.number(), values: yup.array().of( yup.object().shape({ - value: yup.string().required(), - label: yup.string().required(), + value: yup.string().required(T.translate("validation.required")), + label: yup.string().required(T.translate("validation.required")), is_default: yup.boolean(), order: yup.number(), _shouldSave: yup.boolean() @@ -111,29 +111,6 @@ const AddSponsorExtraQuestionPopup = ({ } }, [formik.values]); - const handleAddValue = () => { - const newValue = { - value: "", - label: "", - is_default: false, - _shouldSave: true - }; - - const updatedValues = [...formik.values.values, newValue]; - formik.setFieldValue("values", updatedValues); - }; - - const handleRemoveValue = async (index) => { - const valueToRemove = formik.values.values[index]; - - if (valueToRemove.id) { - deleteSponsorExtraQuestionValue(formik.values.id, valueToRemove.id); - } - - const updatedValues = formik.values.values.filter((_, i) => i !== index); - formik.setFieldValue("values", updatedValues); - }; - const handleValueChange = (index, field, value) => { if (field === "is_default" && value === true) { const updatedValues = formik.values.values.map((v, i) => { @@ -205,7 +182,14 @@ const AddSponsorExtraQuestionPopup = ({ } }; - const renderValueItem = (valueItem, index, provided, snapshot) => ( + const areExtraQuestionsIncomplete = () => { + if (formik.errors.values) return true; + return formik.values.values.some( + (eq) => eq.name?.trim() === "" || eq.label?.trim() === "" + ); + }; + + const renderValueItem = (valueItem, index, provided, snapshot, remove) => ( handleRemoveValue(index)} - edge="end" - sx={{ color: "#666" }} - disableRipple + onClick={() => { + const valueToRemove = formik.values.values[index]; + if (valueToRemove.id) { + deleteSponsorExtraQuestionValue( + formik.values.id, + valueToRemove.id + ); + } + remove(index); + }} > @@ -300,7 +290,9 @@ const AddSponsorExtraQuestionPopup = ({ - {T.translate("edit_sponsor.add_extra_question")} + {extraQuestion.id + ? T.translate("edit_sponsor.edit_extra_question") + : T.translate("edit_sponsor.add_extra_question")} handleClose()} sx={{ mr: 1 }}> @@ -315,7 +307,12 @@ const AddSponsorExtraQuestionPopup = ({ autoComplete="off" > - + - - a.order - b.order - )} - onReorder={handleReorder} - renderItem={renderValueItem} - idKey="id" - updateOrderKey="order" - droppableId="sponsor-extra-question-values" - /> - - - + + {({ push, remove }) => ( + <> + + a.order - b.order + )} + onReorder={handleReorder} + renderItem={(valueItem, index, provided, snapshot) => + renderValueItem( + valueItem, + index, + provided, + snapshot, + remove + ) + } + idKey="id" + updateOrderKey="order" + droppableId="sponsor-extra-question-values" + /> + + + + )} + )} diff --git a/src/components/forms/sponsor-general-form/badge-scan-settings.js b/src/components/forms/sponsor-general-form/badge-scan-settings.js index f9c6e41ad..ef00c54f9 100644 --- a/src/components/forms/sponsor-general-form/badge-scan-settings.js +++ b/src/components/forms/sponsor-general-form/badge-scan-settings.js @@ -40,8 +40,11 @@ const BadgeScanSettings = ({ const selectedCount = currentSettings && currentSettings.columns - ? renderOptions(denormalizeLeadReportSettings(currentSettings.columns)) - .length + ? renderOptions( + denormalizeLeadReportSettings(currentSettings.columns) + ).filter((option) => + availableLeadReportColumns.some((col) => col.value === option.value) + ).length : 0; const handleUpsertSettings = (newValues) => { diff --git a/src/components/forms/sponsor-general-form/extra-questions.js b/src/components/forms/sponsor-general-form/extra-questions.js index a79691505..36d98f699 100644 --- a/src/components/forms/sponsor-general-form/extra-questions.js +++ b/src/components/forms/sponsor-general-form/extra-questions.js @@ -191,6 +191,11 @@ const SponsorExtraQuestions = ({ onEdit={handleEditExtraQuestion} onDelete={handleDeleteExtraQuestion} onReorder={handleReorder} + deleteDialogBody={(name) => + T.translate("edit_sponsor.extra_question_remove_warning", { + name + }) + } /> )} diff --git a/src/components/forms/sponsor-general-form/manage-tier-addons-popup.js b/src/components/forms/sponsor-general-form/manage-tier-addons-popup.js index 8a13e0685..594833426 100644 --- a/src/components/forms/sponsor-general-form/manage-tier-addons-popup.js +++ b/src/components/forms/sponsor-general-form/manage-tier-addons-popup.js @@ -116,6 +116,8 @@ const ManageTierAddonsPopup = ({ onSubmit(valuesToSave, sponsorship.id); }, + validateOnBlur: false, + validateOnChange: false, enableReinitialize: true }); diff --git a/src/components/forms/sponsor-general-form/sponsorship.js b/src/components/forms/sponsor-general-form/sponsorship.js index 701555a80..670667963 100644 --- a/src/components/forms/sponsor-general-form/sponsorship.js +++ b/src/components/forms/sponsor-general-form/sponsorship.js @@ -66,6 +66,12 @@ const Sponsorship = ({ onSponsorshipPaginate(currentPage, perPage, key, dir); }; + const handleSponsorshipDelete = (sponsorshipId) => { + onSponsorshipDelete(sponsorshipId).then(() => + onSponsorshipPaginate(DEFAULT_CURRENT_PAGE, perPage, order, orderDir) + ); + }; + const handleOpenManageAddonsPopup = (sponsorship) => { setSelectedSponsorship(sponsorship); onSponsorshipSelect(sponsorship); @@ -184,7 +190,7 @@ const Sponsorship = ({ orderField="order" perPage={perPage} currentPage={currentPage} - onDelete={onSponsorshipDelete} + onDelete={handleSponsorshipDelete} onPageChange={handlePageChange} onPerPageChange={handlePerPageChange} onSort={handleSort} diff --git a/src/components/menu/index.js b/src/components/menu/index.js index 61177a4a4..0689c4f10 100644 --- a/src/components/menu/index.js +++ b/src/components/menu/index.js @@ -61,6 +61,10 @@ const getGlobalItems = () => [ { name: "form_templates", linkUrl: "form-templates" + }, + { + name: "page_templates", + linkUrl: "page-templates" } ] }, @@ -240,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/components/mui/chip-select-input.js b/src/components/mui/chip-select-input.js index 691d34d6f..75c0dfc52 100644 --- a/src/components/mui/chip-select-input.js +++ b/src/components/mui/chip-select-input.js @@ -86,7 +86,8 @@ const ChipSelectInput = ({ renderValue={(selected) => ( {selected.map((value) => { - const op = availableOptions.find((op) => op.value === value); + const op = availableOptions.find((opt) => opt.value === value); + if (!op) return null; return ( { + if (!validation) return { isValid: true }; + + // validate with yup schema + if ( + validation.schema && + typeof validation.schema.validateSync === "function" + ) { + try { + validation.schema.validateSync(value); + return { isValid: true, message: null }; + } catch (err) { + return { isValid: false, message: err.message }; + } + } + + return { isValid: true }; +}; + // Updated component to handle editable cells with hover edit icon -const EditableCell = ({ value, isEditing, onBlur }) => { +const EditableCell = ({ value, isEditing, onBlur, validation }) => { const [inputValue, setInputValue] = React.useState(value); const [isHovering, setIsHovering] = React.useState(false); + const [error, setError] = React.useState(null); React.useEffect(() => { setInputValue(value); + setError(null); }, [value]); + const handleValidationAndSave = (newValue) => { + const { isValid, message } = validateValue(newValue, validation); + + if (isValid) { + setError(null); + onBlur(newValue, true); + } else { + setError(message); + } + }; + const handleKeyDown = (e) => { if (e.key === "Enter") { e.preventDefault(); - onBlur(inputValue); + handleValidationAndSave(inputValue); } }; @@ -44,14 +76,19 @@ const EditableCell = ({ value, isEditing, onBlur }) => { setInputValue(e.target.value)} + onChange={(e) => { + setInputValue(e.target.value); + if (error) setError(null); + }} onBlur={() => { - onBlur(inputValue); + handleValidationAndSave(inputValue); }} onKeyDown={handleKeyDown} size="small" fullWidth variant="standard" + error={!!error} + helperText={error} /> ); } @@ -150,8 +187,8 @@ const MuiTableEditable = ({ }; // Handler for saving changes when editing is complete - const handleCellBlur = (rowId, columnKey, newValue) => { - if (onCellChange) { + const handleCellBlur = (rowId, columnKey, newValue, isValid) => { + if (onCellChange && isValid) { onCellChange(rowId, columnKey, newValue); } setEditingCell(null); @@ -229,9 +266,15 @@ const MuiTableEditable = ({ editingCell.rowId === row.id && editingCell.columnKey === col.columnKey } - onBlur={(newValue) => - handleCellBlur(row.id, col.columnKey, newValue) + onBlur={(newValue, isValid) => + handleCellBlur( + row.id, + col.columnKey, + newValue, + isValid + ) } + validation={col.validation} /> ) : col.render ? ( col.render(row) diff --git a/src/i18n/en.json b/src/i18n/en.json index 8a04b78c8..506fed838 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", @@ -187,7 +188,8 @@ "submission_invitations": "Submission Invitations", "sponsors_inventory": "Sponsors", "form_templates": "Form Templates", - "inventory": "Inventory" + "inventory": "Inventory", + "page_templates": "Pages" }, "schedule": { "schedule": "Schedule", @@ -2342,6 +2344,7 @@ "extra_question_saved": "Extra Question saved successfully", "extra_question_created": "Extra Question created successfully", "extra_question_deleted": "Extra Question deleted successfully", + "extra_question_remove_warning": "Please verify you want to delete {name}", "remove_warning_ads": "Are you sure you want to delete advertisement ", "remove_warning_materials": "Are you sure you want to delete material ", "remove_warning_social_networks": "Are you sure you want to delete social network ", @@ -2364,6 +2367,7 @@ "add_question": "Add Question", "no_extra_questions": "No extra questions found.", "add_extra_question": "Add Extra Question", + "edit_extra_question": "Edit Extra Question", "save_extra_question": "Save Extra Question", "question_type": "Question Type", "question_id": "Question Identifier", @@ -2479,6 +2483,7 @@ "form_delete_success": "Form successfully deleted.", "unarchive_button": "Unarchive", "archive_button": "Archive", + "remove_form_warning": "Please verify you want to delete {name}", "placeholders": { "search": "Search..." }, @@ -2572,6 +2577,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", @@ -3828,5 +3867,35 @@ "seat_type": "Select a Seat Type", "status": "Select a Status" } + }, + "page_template_list": { + "page_templates": "Page Templates", + "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", + "info_mod": "Info Mod", + "upload_mod": "Upload Mod", + "download_mod": "Download Mod", + "hide_archived": "Hide archived pages", + "no_pages": "No pages found.", + "add_new": "New Page", + "add_template": "Using Template", + "delete_form_template_warning": "Are you sure you want to delete form template ", + "using_duplicate": "Using Duplicate", + "add_form_template": "New Form", + "add_using_global_template": "Using Global Template", + "placeholders": { + "search": "Search" + }, + "page_crud": { + "title": "Create New Page", + "add_info": "Add Info", + "add_doc": "Add Document Download", + "add_media": "Add Media Request", + "no_modules": "No modules added yet.", + "save": "Save Page", + "page_saved": "Page saved successfully.", + "page_created": "Page created successfully." + } } } diff --git a/src/layouts/form-template-item-layout.js b/src/layouts/form-template-item-layout.js index 2ee8c4a40..f3526ebde 100644 --- a/src/layouts/form-template-item-layout.js +++ b/src/layouts/form-template-item-layout.js @@ -16,8 +16,8 @@ import { Switch, Route, withRouter } from "react-router-dom"; import T from "i18n-react/dist/i18n-react"; import { Breadcrumb } from "react-breadcrumbs"; import Restrict from "../routes/restrict"; -import EditFormTemplateItemPage from "../pages/sponsors_inventory/edit-form-template-item-page"; -import FormTemplateItemListPage from "../pages/sponsors_inventory/form-template-item-list-page"; +import EditFormTemplateItemPage from "../pages/sponsors-global/form-templates/edit-form-template-item-page"; +import FormTemplateItemListPage from "../pages/sponsors-global/form-templates/form-template-item-list-page"; import NoMatchPage from "../pages/no-match-page"; const FormTemplateItemLayout = ({ match }) => ( diff --git a/src/layouts/form-template-layout.js b/src/layouts/form-template-layout.js index ead05f5d2..7a9f037ad 100644 --- a/src/layouts/form-template-layout.js +++ b/src/layouts/form-template-layout.js @@ -16,8 +16,8 @@ import { Switch, Route, withRouter } from "react-router-dom"; import T from "i18n-react/dist/i18n-react"; import { Breadcrumb } from "react-breadcrumbs"; import Restrict from "../routes/restrict"; -import FormTemplateListPage from "../pages/sponsors_inventory/form-template-list-page"; -import EditFormTemplatePage from "../pages/sponsors_inventory/edit-form-template-page"; +import FormTemplateListPage from "../pages/sponsors-global/form-templates/form-template-list-page"; +import EditFormTemplatePage from "../pages/sponsors-global/form-templates/edit-form-template-page"; import FormTemplateItemLayout from "./form-template-item-layout"; import NoMatchPage from "../pages/no-match-page"; diff --git a/src/layouts/inventory-item-layout.js b/src/layouts/inventory-item-layout.js index 025166cd3..b012fbe99 100644 --- a/src/layouts/inventory-item-layout.js +++ b/src/layouts/inventory-item-layout.js @@ -16,8 +16,8 @@ import { Switch, Route, withRouter } from "react-router-dom"; import T from "i18n-react/dist/i18n-react"; import { Breadcrumb } from "react-breadcrumbs"; import Restrict from "../routes/restrict"; -import InventoryListPage from "../pages/sponsors_inventory/inventory-list-page"; -import EditInventoryItemPage from "../pages/sponsors_inventory/edit-inventory-item-page"; +import InventoryListPage from "../pages/sponsors-global/inventory/inventory-list-page"; +import EditInventoryItemPage from "../pages/sponsors-global/inventory/edit-inventory-item-page"; import NoMatchPage from "../pages/no-match-page"; const InventoryItemLayout = ({ match }) => ( diff --git a/src/layouts/page-template-layout.js b/src/layouts/page-template-layout.js new file mode 100644 index 000000000..445381daa --- /dev/null +++ b/src/layouts/page-template-layout.js @@ -0,0 +1,55 @@ +/** + * 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 from "react"; +import { Switch, Route, withRouter } from "react-router-dom"; +import T from "i18n-react/dist/i18n-react"; +import { Breadcrumb } from "react-breadcrumbs"; +import Restrict from "../routes/restrict"; +import NoMatchPage from "../pages/no-match-page"; +import EditPageTemplatePage from "../pages/sponsors-global/page-templates/edit-page-template-page"; +import PageTemplateListPage from "../pages/sponsors-global/page-templates/page-template-list-page"; + +const PageTemplateLayout = ({ match }) => ( +
+ + + + + + + +
+); + +export default Restrict(withRouter(PageTemplateLayout), "page-template"); diff --git a/src/layouts/primary-layout.js b/src/layouts/primary-layout.js index 29f46f0b4..086398890 100644 --- a/src/layouts/primary-layout.js +++ b/src/layouts/primary-layout.js @@ -29,6 +29,7 @@ import MediaFileTypeLayout from "./media-file-type-layout"; import SponsoredProjectLayout from "./sponsored-project-layout"; import TagLayout from "./tag-layout"; import SponsorshipLayout from "./sponsorship-layout"; +import PageTemplateLayout from "./page-template-layout"; const PrimaryLayout = ({ match, currentSummit, location, member }) => { let extraClass = "container"; @@ -65,6 +66,7 @@ const PrimaryLayout = ({ match, currentSummit, location, member }) => { + ; 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 { diff --git a/src/pages/sponsors_inventory/edit-form-template-page.js b/src/pages/sponsors-global/form-templates/edit-form-template-page.js similarity index 95% rename from src/pages/sponsors_inventory/edit-form-template-page.js rename to src/pages/sponsors-global/form-templates/edit-form-template-page.js index 495987f62..f026badd7 100644 --- a/src/pages/sponsors_inventory/edit-form-template-page.js +++ b/src/pages/sponsors-global/form-templates/edit-form-template-page.js @@ -15,7 +15,7 @@ import React, { useEffect } from "react"; import { connect } from "react-redux"; import { Breadcrumb } from "react-breadcrumbs"; import T from "i18n-react/dist/i18n-react"; -import FormTemplateForm from "../../components/forms/form-template-form"; +import FormTemplateForm from "../../../components/forms/form-template-form"; import { getFormTemplate, resetFormTemplateForm, @@ -23,7 +23,7 @@ import { deleteFormTemplateMetaFieldType, deleteFormTemplateMetaFieldTypeValue, deleteFormTemplateMaterial -} from "../../actions/form-template-actions"; +} from "../../../actions/form-template-actions"; const EditFormTemplatePage = (props) => { const { diff --git a/src/pages/sponsors_inventory/popup/form-template-from-duplicate-popup.js b/src/pages/sponsors-global/form-templates/form-template-from-duplicate-popup.js similarity index 100% rename from src/pages/sponsors_inventory/popup/form-template-from-duplicate-popup.js rename to src/pages/sponsors-global/form-templates/form-template-from-duplicate-popup.js diff --git a/src/pages/sponsors_inventory/form-template-item-list-page.js b/src/pages/sponsors-global/form-templates/form-template-item-list-page.js similarity index 95% rename from src/pages/sponsors_inventory/form-template-item-list-page.js rename to src/pages/sponsors-global/form-templates/form-template-item-list-page.js index 9eebcc662..16b6acf04 100644 --- a/src/pages/sponsors_inventory/form-template-item-list-page.js +++ b/src/pages/sponsors-global/form-templates/form-template-item-list-page.js @@ -27,7 +27,7 @@ import AddIcon from "@mui/icons-material/Add"; import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; import ImageIcon from "@mui/icons-material/Image"; -import MuiTable from "../../components/mui/table/mui-table"; +import MuiTable from "../../../components/mui/table/mui-table"; import { cloneFromInventoryItem, deleteFormTemplateItem, @@ -39,12 +39,12 @@ import { deleteItemImage, unarchiveFormTemplateItem, archiveFormTemplateItem -} from "../../actions/form-template-item-actions"; -import { getFormTemplate } from "../../actions/form-template-actions"; -import AddFormTemplateItemDialog from "./popup/add-form-template-item-popup"; -import SponsorItemDialog from "./popup/sponsor-inventory-popup"; -import { getInventoryItems } from "../../actions/inventory-item-actions"; -import { DEFAULT_CURRENT_PAGE } from "../../utils/constants"; +} from "../../../actions/form-template-item-actions"; +import { getFormTemplate } from "../../../actions/form-template-actions"; +import AddFormTemplateItemDialog from "./add-form-template-item-popup"; +import SponsorItemDialog from "./sponsor-inventory-popup"; +import { getInventoryItems } from "../../../actions/inventory-item-actions"; +import { DEFAULT_CURRENT_PAGE } from "../../../utils/constants"; const FormTemplateItemListPage = ({ formTemplateId, diff --git a/src/pages/sponsors_inventory/form-template-list-page.js b/src/pages/sponsors-global/form-templates/form-template-list-page.js similarity index 95% rename from src/pages/sponsors_inventory/form-template-list-page.js rename to src/pages/sponsors-global/form-templates/form-template-list-page.js index e8882fe04..515561cdc 100644 --- a/src/pages/sponsors_inventory/form-template-list-page.js +++ b/src/pages/sponsors-global/form-templates/form-template-list-page.js @@ -37,12 +37,12 @@ import { resetFormTemplateForm, saveFormTemplate, unarchiveFormTemplate -} from "../../actions/form-template-actions"; -import MuiTable from "../../components/mui/table/mui-table"; -import FormTemplateDialog from "./popup/form-template-popup"; -import history from "../../history"; -import FormTemplateFromDuplicateDialog from "./popup/form-template-from-duplicate-popup"; -import { DEFAULT_CURRENT_PAGE } from "../../utils/constants"; +} from "../../../actions/form-template-actions"; +import MuiTable from "../../../components/mui/table/mui-table"; +import FormTemplateDialog from "./form-template-popup"; +import history from "../../../history"; +import FormTemplateFromDuplicateDialog from "./form-template-from-duplicate-popup"; +import { DEFAULT_CURRENT_PAGE } from "../../../utils/constants"; const FormTemplateListPage = ({ formTemplates, @@ -163,7 +163,14 @@ const FormTemplateListPage = ({ item.is_archived ? unarchiveFormTemplate(item) : archiveFormTemplate(item); const handleHideArchivedForms = (value) => { - getFormTemplates(term, currentPage, perPage, order, orderDir, value); + getFormTemplates( + term, + DEFAULT_CURRENT_PAGE, + perPage, + order, + orderDir, + value + ); }; const columns = [ diff --git a/src/pages/sponsors_inventory/popup/form-template-popup.js b/src/pages/sponsors-global/form-templates/form-template-popup.js similarity index 99% rename from src/pages/sponsors_inventory/popup/form-template-popup.js rename to src/pages/sponsors-global/form-templates/form-template-popup.js index 88b97044d..d47e94488 100644 --- a/src/pages/sponsors_inventory/popup/form-template-popup.js +++ b/src/pages/sponsors-global/form-templates/form-template-popup.js @@ -20,7 +20,7 @@ import CloseIcon from "@mui/icons-material/Close"; import { useFormik, FormikProvider, FieldArray } from "formik"; import * as yup from "yup"; import showConfirmDialog from "../../../components/mui/showConfirmDialog"; -import MetaFieldValues from "./meta-field-values"; +import MetaFieldValues from "../shared/meta-field-values"; import MuiFormikTextField from "../../../components/mui/formik-inputs/mui-formik-textfield"; import FormikTextEditor from "../../../components/inputs/formik-text-editor"; import MuiFormikSelect from "../../../components/mui/formik-inputs/mui-formik-select"; diff --git a/src/pages/sponsors_inventory/popup/sponsor-inventory-popup.js b/src/pages/sponsors-global/form-templates/sponsor-inventory-popup.js similarity index 89% rename from src/pages/sponsors_inventory/popup/sponsor-inventory-popup.js rename to src/pages/sponsors-global/form-templates/sponsor-inventory-popup.js index e59f3a254..5b87d555a 100644 --- a/src/pages/sponsors_inventory/popup/sponsor-inventory-popup.js +++ b/src/pages/sponsors-global/form-templates/sponsor-inventory-popup.js @@ -28,7 +28,7 @@ import { METAFIELD_TYPES } from "../../../utils/constants"; import showConfirmDialog from "../../../components/mui/showConfirmDialog"; -import MetaFieldValues from "./meta-field-values"; +import MetaFieldValues from "../shared/meta-field-values"; import MuiFormikTextField from "../../../components/mui/formik-inputs/mui-formik-textfield"; import useScrollToError from "../../../hooks/useScrollToError"; import MuiFormikSelect from "../../../components/mui/formik-inputs/mui-formik-select"; @@ -69,8 +69,8 @@ const SponsorItemDialog = ({ name: "", type: "Text", is_required: false, - minimum_quantity: null, - maximum_quantity: null, + minimum_quantity: 0, + maximum_quantity: 0, values: [] } ], @@ -97,11 +97,12 @@ const SponsorItemDialog = ({ yup.object().shape({ name: yup .string() - .when(["values", "minimum_quantity", "maximum_quantity"], { - is: (values, minQty, maxQty) => { + .when(["type", "values", "minimum_quantity", "maximum_quantity"], { + is: (type, values, minQty, maxQty) => { // required only if has values or quantities const hasValues = values && values.length > 0; - const hasQuantities = minQty !== null || maxQty !== null; + const hasQuantities = + type === "Quantity" && (minQty != null || maxQty != null); return hasValues || hasQuantities; }, then: (schema) => @@ -149,7 +150,6 @@ const SponsorItemDialog = ({ }) ) }), - enableReinitialize: true, onSubmit: (values) => onSave(values) }); @@ -238,10 +238,9 @@ const SponsorItemDialog = ({ onClose(); }; - const isMetafieldIncomplete = (field) => { + const areMetafieldsIncomplete = () => { if (formik.errors.meta_fields) return true; - if (field.name === "") return true; - return false; + return formik.values.meta_fields.some((f) => f.name?.trim() === ""); }; return ( @@ -395,6 +394,7 @@ const SponsorItemDialog = ({ container spacing={2} sx={{ alignItems: "center" }} + // eslint-disable-next-line key={field} > @@ -494,48 +494,34 @@ const SponsorItemDialog = ({ sx={{ alignItems: "start", my: 2 }} > - - - + - - - + )} @@ -573,7 +559,7 @@ const SponsorItemDialog = ({ + +
+
+ + + {pageTemplates.length === 0 && ( + + {T.translate("page_template_list.no_pages")} + + )} + + {pageTemplates.length > 0 && ( +
+ +
+ )} +
+ setPageTemplateId(null)} + onSave={savePageTemplate} + /> + + ); +}; + +const mapStateToProps = ({ pageTemplateListState }) => ({ + ...pageTemplateListState +}); + +export default connect(mapStateToProps, { + getPageTemplates, + archivePageTemplate, + unarchivePageTemplate, + savePageTemplate +})(PageTemplateListPage); diff --git a/src/pages/sponsors-global/page-templates/page-template-popup.js b/src/pages/sponsors-global/page-templates/page-template-popup.js new file mode 100644 index 000000000..6fc8f89c6 --- /dev/null +++ b/src/pages/sponsors-global/page-templates/page-template-popup.js @@ -0,0 +1,154 @@ +import React from "react"; +import T from "i18n-react/dist/i18n-react"; +import { connect } from "react-redux"; +import PropTypes from "prop-types"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Grid2, + IconButton, + Typography +} from "@mui/material"; +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"; + +const PageTemplatePopup = ({ pageTemplate, open, onClose, onSave }) => { + const handleClose = () => { + onClose(); + }; + + const handleAddInfo = () => { + console.log("ADD INFO"); + }; + + const handleAddDocument = () => { + console.log("ADD DOCUMENT"); + }; + + const handleAddMedia = () => { + console.log("ADD MEDIA"); + }; + + const formik = useFormik({ + initialValues: { + ...pageTemplate + }, + validationSchema: yup.object().shape({ + code: yup.string().required(T.translate("validation.required")), + name: yup.string().required(T.translate("validation.required")) + }), + enableReinitialize: true, + onSubmit: (values) => { + onSave(values); + } + }); + + return ( + + + + {T.translate("page_template_list.page_crud.title")} + + handleClose()} sx={{ mr: 1 }}> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {T.translate("page_template_list.page_crud.no_modules")} + + + + + + + + + + ); +}; + +PageTemplatePopup.propTypes = { + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired +}; + +const mapStateToProps = ({ currentPageTemplateState }) => ({ + ...currentPageTemplateState +}); + +export default connect(mapStateToProps, {})(PageTemplatePopup); diff --git a/src/pages/sponsors_inventory/popup/meta-field-values.js b/src/pages/sponsors-global/shared/meta-field-values.js similarity index 100% rename from src/pages/sponsors_inventory/popup/meta-field-values.js rename to src/pages/sponsors-global/shared/meta-field-values.js diff --git a/src/pages/sponsors/components/additional-input.js b/src/pages/sponsors/components/additional-input.js index 6386f6795..96b9ed74e 100644 --- a/src/pages/sponsors/components/additional-input.js +++ b/src/pages/sponsors/components/additional-input.js @@ -14,7 +14,7 @@ import { import T from "i18n-react"; import DeleteIcon from "@mui/icons-material/Delete"; import AddIcon from "@mui/icons-material/Add"; -import MetaFieldValues from "../../sponsors_inventory/popup/meta-field-values"; +import MetaFieldValues from "../../sponsors-global/shared/meta-field-values"; import { METAFIELD_TYPES, METAFIELD_TYPES_WITH_OPTIONS diff --git a/src/pages/sponsors/sponsor-form-item-list-page/components/item-form.js b/src/pages/sponsors/sponsor-form-item-list-page/components/item-form.js index fffd3183f..b442bbbac 100644 --- a/src/pages/sponsors/sponsor-form-item-list-page/components/item-form.js +++ b/src/pages/sponsors/sponsor-form-item-list-page/components/item-form.js @@ -28,6 +28,19 @@ const buildInitialValues = (data) => { addIssAfterDateFieldValidator(); +const numberValidation = () => + yup.number().typeError(T.translate("validation.number")); + +const decimalValidation = () => + yup + .number() + .typeError(T.translate("validation.number")) + .min(0, T.translate("validation.number_positive")) + .test("max-decimals", T.translate("validation.two_decimals"), (value) => { + if (value === undefined || value === null) return true; + return /^\d+(\.\d{1,2})?$/.test(value.toString()); + }); + const ItemForm = ({ initialValues, onSubmit }) => { const formik = useFormik({ initialValues: buildInitialValues(initialValues), @@ -41,18 +54,18 @@ const ItemForm = ({ initialValues, onSubmit }) => { description: yup .string(T.translate("validation.string")) .required(T.translate("validation.required")), - early_bird_rate: yup - .number(T.translate("validation.number")) - .required(T.translate("validation.required")), - standard_rate: yup - .number(T.translate("validation.number")) - .required(T.translate("validation.required")), - onsite_rate: yup - .number(T.translate("validation.number")) - .required(T.translate("validation.required")), - default_quantity: yup - .number(T.translate("validation.number")) - .required(T.translate("validation.required")) + early_bird_rate: decimalValidation(), + standard_rate: decimalValidation(), + onsite_rate: decimalValidation(), + default_quantity: numberValidation() + .integer(T.translate("validation.integer")) + .min(0, T.translate("validation.number_positive")), + quantity_limit_per_sponsor: numberValidation() + .integer(T.translate("validation.integer")) + .min(0, T.translate("validation.number_positive")), + quantity_limit_per_show: numberValidation() + .integer(T.translate("validation.integer")) + .min(0, T.translate("validation.number_positive")) }), onSubmit, enableReinitialize: true diff --git a/src/pages/sponsors/sponsor-form-item-list-page/index.js b/src/pages/sponsors/sponsor-form-item-list-page/index.js index 8ab191468..344f46e00 100644 --- a/src/pages/sponsors/sponsor-form-item-list-page/index.js +++ b/src/pages/sponsors/sponsor-form-item-list-page/index.js @@ -15,6 +15,7 @@ import React, { useEffect, useState } from "react"; import { Breadcrumb } from "react-breadcrumbs"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; +import * as yup from "yup"; import { Alert, Box, @@ -40,6 +41,7 @@ import ItemPopup from "./components/item-popup"; import InventoryPopup from "./components/inventory-popup"; import MuiTableEditable from "../../../components/mui/editable-table/mui-table-editable"; import { parsePrice } from "../../../utils/currency"; +import { DEFAULT_CURRENT_PAGE } from "../../../utils/constants"; const SponsorFormItemListPage = ({ match, @@ -75,7 +77,7 @@ const SponsorFormItemListPage = ({ const handleHideArchivedForms = (ev) => { getSponsorFormItems( formId, - currentPage, + DEFAULT_CURRENT_PAGE, perPage, order, orderDir, @@ -111,6 +113,37 @@ const SponsorFormItemListPage = ({ setOpenPopup("inventory"); }; + const rateCellValidation = () => + yup + .number() + // allow $ at the start + .transform((value, originalValue) => { + if (typeof originalValue === "string") { + const cleaned = originalValue.replace(/^\$/, ""); + return cleaned === "" ? undefined : parseFloat(cleaned); + } + return value; + }) + // check if there's letters or characters + .test({ + name: "valid-format", + message: T.translate("validation.number"), + test: (value, { originalValue }) => { + if ( + originalValue === undefined || + originalValue === null || + originalValue === "" + ) + return true; + return /^\$?-?\d+(\.\d+)?$/.test(originalValue); + } + }) + .min(0, T.translate("validation.number_positive")) + .test("max-decimals", T.translate("validation.two_decimals"), (value) => { + if (value === undefined || value === null) return true; + return /^\d+(\.\d{1,2})?$/.test(value.toString()); + }); + const columns = [ { columnKey: "code", @@ -126,19 +159,28 @@ const SponsorFormItemListPage = ({ columnKey: "early_bird_rate", header: T.translate("sponsor_form_item_list.early_bird_rate"), sortable: true, - editable: true + editable: true, + validation: { + schema: rateCellValidation() + } }, { columnKey: "standard_rate", header: T.translate("sponsor_form_item_list.standard_rate"), sortable: true, - editable: true + editable: true, + validation: { + schema: rateCellValidation() + } }, { columnKey: "onsite_rate", header: T.translate("sponsor_form_item_list.onsite_rate"), sortable: true, - editable: true + editable: true, + validation: { + schema: rateCellValidation() + } }, { columnKey: "default_quantity", diff --git a/src/pages/sponsors/sponsor-forms-list-page/index.js b/src/pages/sponsors/sponsor-forms-list-page/index.js index 5c5d62a84..17829f18a 100644 --- a/src/pages/sponsors/sponsor-forms-list-page/index.js +++ b/src/pages/sponsors/sponsor-forms-list-page/index.js @@ -36,6 +36,7 @@ import SearchInput from "../../../components/mui/search-input"; import GlobalTemplatePopup from "./components/global-template/global-template-popup"; import FormTemplatePopup from "./components/form-template/form-template-popup"; import MuiTable from "../../../components/mui/table/mui-table"; +import { DEFAULT_CURRENT_PAGE } from "../../../utils/constants"; const SponsorFormsListPage = ({ sponsorForms, @@ -61,7 +62,16 @@ const SponsorFormsListPage = ({ const handlePageChange = (page) => { getSponsorForms(term, page, perPage, order, orderDir, hideArchived); }; - + const handlePerPageChange = (newPerPage) => { + getSponsorForms( + term, + currentPage, + newPerPage, + order, + orderDir, + hideArchived + ); + }; const handleSort = (key, dir) => { getSponsorForms(term, currentPage, perPage, key, dir, hideArchived); }; @@ -99,7 +109,7 @@ const SponsorFormsListPage = ({ const handleHideArchivedForms = (ev) => { getSponsorForms( term, - currentPage, + DEFAULT_CURRENT_PAGE, perPage, order, orderDir, @@ -224,7 +234,11 @@ const SponsorFormsListPage = ({ totalRows={totalCount} currentPage={currentPage} onDelete={handleRowDelete} + deleteDialogBody={(name) => + T.translate("sponsor_forms.remove_form_warning", { name }) + } onPageChange={handlePageChange} + onPerPageChange={handlePerPageChange} onSort={handleSort} onEdit={handleRowEdit} onArchive={handleArchiveItem} diff --git a/src/pages/sponsors/sponsor-forms-tab/index.js b/src/pages/sponsors/sponsor-forms-tab/index.js index 91f56a286..06920a737 100644 --- a/src/pages/sponsors/sponsor-forms-tab/index.js +++ b/src/pages/sponsors/sponsor-forms-tab/index.js @@ -37,6 +37,7 @@ import SearchInput from "../../../components/mui/search-input"; import MuiTable from "../../../components/mui/table/mui-table"; import AddSponsorFormTemplatePopup from "./components/add-sponsor-form-template-popup"; import CustomizedFormPopup from "./components/customized-form/customized-form-popup"; +import { DEFAULT_CURRENT_PAGE } from "../../../utils/constants"; const SponsorFormsTab = ({ term, @@ -124,7 +125,7 @@ const SponsorFormsTab = ({ const handleHideArchivedForms = (ev) => { getSponsorManagedForms( term, - managedForms.currentPage, + DEFAULT_CURRENT_PAGE, managedForms.perPage, managedForms.order, managedForms.orderDir, @@ -132,7 +133,7 @@ const SponsorFormsTab = ({ ); getSponsorCustomizedForms( term, - customizedForms.currentPage, + DEFAULT_CURRENT_PAGE, customizedForms.perPage, customizedForms.order, customizedForms.orderDir, diff --git a/src/pages/sponsors/sponsor-list-page.js b/src/pages/sponsors/sponsor-list-page.js index 3fc35351f..e10100bc0 100644 --- a/src/pages/sponsors/sponsor-list-page.js +++ b/src/pages/sponsors/sponsor-list-page.js @@ -117,7 +117,7 @@ const SponsorListPage = ({ { columnKey: "company_name", header: T.translate("sponsor_list.company") }, { columnKey: "sponsorships", - header: T.translate("sponsor_list.sponsorship"), + header: T.translate("sponsor_list.sponsorships"), render: (row) => row.sponsorships.map((s) => ( diff --git a/src/pages/sponsors/sponsor-pages-list-page/components/global-page/global-page-popup.js b/src/pages/sponsors/sponsor-pages-list-page/components/global-page/global-page-popup.js new file mode 100644 index 000000000..d085e2856 --- /dev/null +++ b/src/pages/sponsors/sponsor-pages-list-page/components/global-page/global-page-popup.js @@ -0,0 +1,55 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { Dialog } from "@mui/material"; +import SelectPagesDialog from "./select-pages-dialog"; +import SelectSponsorshipsDialog from "../../../sponsor-forms-list-page/components/global-template/select-sponsorships-dialog"; +import { cloneGlobalPage } from "../../../../../actions/sponsor-pages-actions"; + +const GlobalPagePopup = ({ open, onClose, cloneGlobalPage }) => { + 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-forms-list-reducer.js b/src/reducers/sponsors/sponsor-forms-list-reducer.js index c96e239b6..151b82a5a 100644 --- a/src/reducers/sponsors/sponsor-forms-list-reducer.js +++ b/src/reducers/sponsors/sponsor-forms-list-reducer.js @@ -78,7 +78,7 @@ const sponsorFormsListReducer = (state = DEFAULT_STATE, action) => { return DEFAULT_STATE; } case REQUEST_SPONSOR_FORMS: { - const { order, orderDir, page, term, hideArchived } = payload; + const { order, orderDir, page, perPage, term, hideArchived } = payload; return { ...state, @@ -86,6 +86,7 @@ const sponsorFormsListReducer = (state = DEFAULT_STATE, action) => { orderDir, sponsorForms: [], currentPage: page, + perPage, term, hideArchived }; 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/reducers/sponsors_inventory/page-template-list-reducer.js b/src/reducers/sponsors_inventory/page-template-list-reducer.js new file mode 100644 index 000000000..e2ff363d0 --- /dev/null +++ b/src/reducers/sponsors_inventory/page-template-list-reducer.js @@ -0,0 +1,124 @@ +/** + * 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 { LOGOUT_USER } from "openstack-uicore-foundation/lib/security/actions"; +import { + REQUEST_PAGE_TEMPLATES, + RECEIVE_PAGE_TEMPLATES, + PAGE_TEMPLATE_DELETED, + PAGE_TEMPLATE_ARCHIVED, + PAGE_TEMPLATE_UNARCHIVED +} from "../../actions/page-template-actions"; + +const DEFAULT_STATE = { + pageTemplates: [], + term: null, + order: "name", + orderDir: 1, + currentPage: 1, + lastPage: 1, + perPage: 10, + totalPageTemplates: 0, + hideArchived: false +}; + +const pageTemplateListReducer = (state = DEFAULT_STATE, action = {}) => { + const { type, payload } = action; + switch (type) { + case LOGOUT_USER: { + return DEFAULT_STATE; + } + case REQUEST_PAGE_TEMPLATES: { + const { order, orderDir, page, perPage, ...rest } = payload; + + if ( + order !== state.order || + orderDir !== state.orderDir || + page !== state.currentPage + ) { + // if the change was in page or order, keep selection + return { + ...state, + order, + orderDir, + currentPage: page, + ...rest + }; + } + + return { + ...state, + order, + orderDir, + pageTemplates: [], + currentPage: page, + perPage, + ...rest + }; + } + case RECEIVE_PAGE_TEMPLATES: { + const { current_page, total, last_page } = payload.response; + + const pageTemplates = payload.response.data.map((a) => ({ + id: a.id, + code: a.code, + name: a.name, + 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, + pageTemplates, + currentPage: current_page, + totalPageTemplates: total, + lastPage: last_page + }; + } + case PAGE_TEMPLATE_DELETED: { + const { pageTemplateId } = payload; + return { + ...state, + pageTemplates: state.pageTemplates.filter( + (a) => a.id !== pageTemplateId + ) + }; + } + case PAGE_TEMPLATE_ARCHIVED: { + const updatedFormTemplate = payload.response; + + const updatedPageTemplates = state.pageTemplates.map((item) => + item.id === updatedFormTemplate.id + ? { ...item, is_archived: true } + : item + ); + return { ...state, pageTemplates: updatedPageTemplates }; + } + case PAGE_TEMPLATE_UNARCHIVED: { + const updatedFormTemplateId = payload; + + const updatedPageTemplates = state.pageTemplates.map((item) => + item.id === updatedFormTemplateId + ? { ...item, is_archived: false } + : item + ); + return { ...state, pageTemplates: updatedPageTemplates }; + } + default: + return state; + } +}; + +export default pageTemplateListReducer; diff --git a/src/store.js b/src/store.js index 64762f8ac..784b3e1e3 100644 --- a/src/store.js +++ b/src/store.js @@ -158,12 +158,14 @@ import formTemplateReducer from "./reducers/sponsors_inventory/form-template-red import formTemplateListReducer from "./reducers/sponsors_inventory/form-template-list-reducer.js"; import formTemplateItemReducer from "./reducers/sponsors_inventory/form-template-item-reducer.js"; import formTemplateItemListReducer from "./reducers/sponsors_inventory/form-template-item-list-reducer.js"; +import pageTemplateListReducer from "./reducers/sponsors_inventory/page-template-list-reducer.js"; import sponsorSettingsReducer from "./reducers/sponsor_settings/sponsor-settings-reducer"; import eventRSVPListReducer from "./reducers/rsvps/event-rsvp-list-reducer.js"; import eventRSVPInvitationListReducer from "./reducers/rsvps/event-rsvp-invitation-list-reducer.js"; import eventRSVPReducer from "./reducers/events/event-rsvp-reducer.js"; import sponsorPageFormsListReducer from "./reducers/sponsors/sponsor-page-forms-list-reducer.js"; import sponsorCustomizedFormReducer from "./reducers/sponsors/sponsor-customized-form-reducer.js"; +import sponsorPagesListReducer from "./reducers/sponsors/sponsor-pages-list-reducer.js"; // default: localStorage if web, AsyncStorage if react-native @@ -247,6 +249,7 @@ const reducers = persistCombineReducers(config, { currentSponsorState: sponsorReducer, sponsorFormsListState: sponsorFormsListReducer, sponsorFormItemsListState: sponsorFormItemsListReducer, + sponsorPagesListState: sponsorPagesListReducer, sponsorUsersListState: sponsorUsersListReducer, sponsorPageFormsListState: sponsorPageFormsListReducer, sponsorCustomizedFormState: sponsorCustomizedFormReducer, @@ -322,7 +325,8 @@ const reducers = persistCombineReducers(config, { currentFormTemplateListState: formTemplateListReducer, currentFormTemplateItemState: formTemplateItemReducer, currentFormTemplateItemListState: formTemplateItemListReducer, - sponsorSettingsState: sponsorSettingsReducer + sponsorSettingsState: sponsorSettingsReducer, + pageTemplateListState: pageTemplateListReducer }); const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; diff --git a/yarn.lock b/yarn.lock index eb1fbd9a8..823369b55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10342,10 +10342,10 @@ open@^8.0.9: is-docker "^2.1.1" is-wsl "^2.2.0" -openstack-uicore-foundation@4.2.14: - version "4.2.14" - resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-4.2.14.tgz#d759a6358ede869f356fdf387065632b341bcf6a" - integrity sha512-ECwPZ7QOUrhW/fKbyWUfsYx/x207WPuLx9VfDLS2i46p3CoD+z6XT2a4AoUzYBoOa9iRdhXAtJUsymN8sGIzYA== +openstack-uicore-foundation@4.2.19: + version "4.2.19" + resolved "https://registry.npmjs.org/openstack-uicore-foundation/-/openstack-uicore-foundation-4.2.19.tgz#40ae09230b483a3279592bdeb0e7c164af4e5c97" + integrity sha512-spRHx76SlFmJqUtsUz786pnX+oaco/gDH7LJ7u5MIj5c9VcCo9rkeBfwl7htCLaCC68msuRGwlrMJpkSsY6gxw== optionator@^0.9.1: version "0.9.4" @@ -12512,6 +12512,11 @@ sourcemapped-stacktrace@^1.1.6: dependencies: source-map "0.5.6" +spark-md5@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc" + integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw== + spdx-correct@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c"