diff --git a/.env.example b/.env.example index e3779db3f..7178b9069 100644 --- a/.env.example +++ b/.env.example @@ -17,7 +17,7 @@ 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" SPONSOR_PAGES_API_URL=https://sponsor-pages-api.dev.fnopen.com -SPONSOR_PAGES_SCOPES="page-template/read page-template/write" +SPONSOR_PAGES_SCOPES="page-template/read page-template/write show-page/read show-page/write" SCOPES="profile openid offline_access ${SPONSOR_USERS_API_SCOPES} ${PURCHASES_API_SCOPES} ${EMAIL_SCOPES} ${FILE_UPLOAD_SCOPES} ${SPONSOR_PAGES_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" GOOGLE_API_KEY= ALLOWED_USER_GROUPS="super-admins administrators summit-front-end-administrators summit-room-administrators track-chairs-admins sponsors" diff --git a/src/actions/media-file-type-actions.js b/src/actions/media-file-type-actions.js index d9fd1153a..55a8e825e 100644 --- a/src/actions/media-file-type-actions.js +++ b/src/actions/media-file-type-actions.js @@ -9,9 +9,10 @@ * 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 history from "../history"; +import _ from "lodash"; import { getRequest, putRequest, @@ -23,9 +24,18 @@ import { showMessage, showSuccessMessage, authErrorHandler, + fetchResponseHandler, + fetchErrorHandler, escapeFilterValue } from "openstack-uicore-foundation/lib/utils/actions"; +import URI from "urijs"; +import history from "../history"; import { getAccessTokenSafely } from "../utils/methods"; +import { + DEBOUNCE_WAIT, + DEFAULT_PER_PAGE, + FIVE_PER_PAGE +} from "../utils/constants"; export const REQUEST_MEDIA_FILE_TYPES = "REQUEST_MEDIA_FILE_TYPES"; export const RECEIVE_MEDIA_FILE_TYPES = "RECEIVE_MEDIA_FILE_TYPES"; @@ -39,15 +49,21 @@ export const MEDIA_FILE_TYPE_ADDED = "MEDIA_FILE_TYPE_ADDED"; export const MEDIA_FILE_TYPE_DELETED = "MEDIA_FILE_TYPE_DELETED"; export const getMediaFileTypes = - (term = null, page = 1, perPage = 10, order = "id", orderDir = 1) => - async (dispatch, getState) => { + ( + term = null, + page = 1, + perPage = DEFAULT_PER_PAGE, + order = "id", + orderDir = 1 + ) => + async (dispatch) => { const accessToken = await getAccessTokenSafely(); const filter = []; dispatch(startLoading()); const params = { - page: page, + page, per_page: perPage, access_token: accessToken }; @@ -64,7 +80,7 @@ export const getMediaFileTypes = // order if (order != null && orderDir != null) { const orderDirSign = orderDir === 1 ? "" : "-"; - params["order"] = `${orderDirSign}${order}`; + params.order = `${orderDirSign}${order}`; } return getRequest( @@ -78,7 +94,7 @@ export const getMediaFileTypes = }); }; -export const getAllMediaFileTypes = () => async (dispatch, getState) => { +export const getAllMediaFileTypes = () => async (dispatch) => { const accessToken = await getAccessTokenSafely(); dispatch(startLoading()); @@ -100,33 +116,32 @@ export const getAllMediaFileTypes = () => async (dispatch, getState) => { }); }; -export const getMediaFileType = - (mediaFileTypeId) => async (dispatch, getState) => { - const accessToken = await getAccessTokenSafely(); - - dispatch(startLoading()); +export const getMediaFileType = (mediaFileTypeId) => async (dispatch) => { + const accessToken = await getAccessTokenSafely(); - const params = { - access_token: accessToken - }; + dispatch(startLoading()); - return getRequest( - null, - createAction(RECEIVE_MEDIA_FILE_TYPE), - `${window.API_BASE_URL}/api/v1/summit-media-file-types/${mediaFileTypeId}`, - authErrorHandler - )(params)(dispatch).then(() => { - dispatch(stopLoading()); - }); + const params = { + access_token: accessToken }; -export const resetMediaFileTypeForm = () => (dispatch, getState) => { + return getRequest( + null, + createAction(RECEIVE_MEDIA_FILE_TYPE), + `${window.API_BASE_URL}/api/v1/summit-media-file-types/${mediaFileTypeId}`, + authErrorHandler + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); +}; + +export const resetMediaFileTypeForm = () => (dispatch) => { dispatch(createAction(RESET_MEDIA_FILE_TYPE_FORM)({})); }; export const saveMediaFileType = (entity, noAlert = false) => - async (dispatch, getState) => { + async (dispatch) => { const accessToken = await getAccessTokenSafely(); dispatch(startLoading()); @@ -142,7 +157,7 @@ export const saveMediaFileType = normalizedEntity, authErrorHandler, entity - )(params)(dispatch).then((payload) => { + )(params)(dispatch).then(() => { if (!noAlert) dispatch(showSuccessMessage(T.translate("media_file_type.saved"))); else dispatch(stopLoading()); @@ -171,33 +186,54 @@ export const saveMediaFileType = } }; -export const deleteMediaFileType = - (mediaFileTypeId) => async (dispatch, getState) => { - const accessToken = await getAccessTokenSafely(); - - const params = { - access_token: accessToken - }; +export const deleteMediaFileType = (mediaFileTypeId) => async (dispatch) => { + const accessToken = await getAccessTokenSafely(); - return deleteRequest( - null, - createAction(MEDIA_FILE_TYPE_DELETED)({ mediaFileTypeId }), - `${window.API_BASE_URL}/api/v1/summit-media-file-types/${mediaFileTypeId}`, - null, - authErrorHandler - )(params)(dispatch).then(() => { - dispatch(stopLoading()); - }); + const params = { + access_token: accessToken }; + return deleteRequest( + null, + createAction(MEDIA_FILE_TYPE_DELETED)({ mediaFileTypeId }), + `${window.API_BASE_URL}/api/v1/summit-media-file-types/${mediaFileTypeId}`, + null, + authErrorHandler + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); +}; + const normalizeEntity = (entity) => { const normalizedEntity = { ...entity }; - delete normalizedEntity["id"]; - delete normalizedEntity["created"]; - delete normalizedEntity["modified"]; + delete normalizedEntity.id; + delete normalizedEntity.created; + delete normalizedEntity.modified; normalizedEntity.allowed_extensions = entity.allowed_extensions.split(","); return normalizedEntity; }; + +export const queryMediaFileTypes = _.debounce(async (input, callback) => { + const accessToken = await getAccessTokenSafely(); + const apiUrl = URI(`${window.API_BASE_URL}/api/v1/summit-media-file-types`); + + apiUrl.addQuery("access_token", accessToken); + apiUrl.addQuery("order", "name"); + apiUrl.addQuery("per_page", FIVE_PER_PAGE); + + if (input) { + input = escapeFilterValue(input); + apiUrl.addQuery("filter[]", `name=@${input}`); + } + + fetch(apiUrl.toString()) + .then(fetchResponseHandler) + .then((json) => { + const options = [...json.data]; + callback(options); + }) + .catch(fetchErrorHandler); +}, DEBOUNCE_WAIT); diff --git a/src/actions/page-template-actions.js b/src/actions/page-template-actions.js index 363230881..9283195ef 100644 --- a/src/actions/page-template-actions.js +++ b/src/actions/page-template-actions.js @@ -12,6 +12,7 @@ * */ import T from "i18n-react/dist/i18n-react"; +import moment from "moment-timezone"; import { getRequest, putRequest, @@ -27,7 +28,8 @@ import { getAccessTokenSafely } from "../utils/methods"; import { DEFAULT_CURRENT_PAGE, DEFAULT_ORDER_DIR, - DEFAULT_PER_PAGE + DEFAULT_PER_PAGE, + PAGES_MODULE_KINDS } from "../utils/constants"; import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions"; @@ -143,7 +145,28 @@ export const resetPageTemplateForm = () => (dispatch) => { const normalizeEntity = (entity) => { const normalizedEntity = { ...entity }; - normalizedEntity.modules = []; + normalizedEntity.modules = entity.modules.map((module) => { + const normalizedModule = { ...module }; + + if (module.kind === PAGES_MODULE_KINDS.MEDIA && module.upload_deadline) { + normalizedModule.upload_deadline = moment + .utc(module.upload_deadline) + .unix(); + } + + if (module.kind === PAGES_MODULE_KINDS.MEDIA && module.file_type_id) { + normalizedModule.file_type_id = + module.file_type_id?.value || module.file_type_id; + } + + if (module.kind === PAGES_MODULE_KINDS.DOCUMENT && module.file) { + normalizedModule.file = module.file[0] || null; + } + + delete normalizedModule._tempId; + + return normalizedModule; + }); return normalizedEntity; }; diff --git a/src/actions/sponsor-pages-actions.js b/src/actions/sponsor-pages-actions.js new file mode 100644 index 000000000..cb7355d1d --- /dev/null +++ b/src/actions/sponsor-pages-actions.js @@ -0,0 +1,135 @@ +/** + * 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 { getSponsorForms } from "./sponsor-forms-actions"; +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") + }) + ); + }) + .finally(() => dispatch(stopLoading())); + }; diff --git a/src/components/inputs/formik-text-editor.js b/src/components/inputs/formik-text-editor.js index 346fb23cb..9098b6088 100644 --- a/src/components/inputs/formik-text-editor.js +++ b/src/components/inputs/formik-text-editor.js @@ -1,24 +1,18 @@ import React from "react"; import TextEditorV3 from "openstack-uicore-foundation/lib/components/inputs/editor-input-v3"; -import { useFormikContext } from "formik"; -import normalizeJoditEmpty from "./utils/normalizeJoditEmpty"; +import { useField } from "formik"; const FormikTextEditor = ({ name, ...props }) => { - const { values, errors, touched, setFieldValue, setFieldTouched } = - useFormikContext(); + const [field, meta, helpers] = useField(name); return ( { - const stringValue = normalizeJoditEmpty(e.target.value); - - setFieldValue(name, stringValue); - }} - onBlur={() => setFieldTouched(name, true)} - error={touched?.[name] && errors?.[name] ? errors?.[name] : ""} + value={field.value} + onChange={(e) => helpers.setValue(e.target.value)} + onBlur={() => helpers.setTouched(true)} + error={meta.touched && Boolean(meta.error) ? meta.error : ""} license={process.env.JODIT_LICENSE_KEY} {...props} /> 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/components/mui/formik-inputs/mui-formik-async-select.js b/src/components/mui/formik-inputs/mui-formik-async-select.js index 7ecb8cf9c..852417e1a 100644 --- a/src/components/mui/formik-inputs/mui-formik-async-select.js +++ b/src/components/mui/formik-inputs/mui-formik-async-select.js @@ -53,7 +53,7 @@ const MuiFormikAsyncAutocomplete = ({ } }, [searchTerm]); - // precargar con vacĂ­o + // preload empty useEffect(() => { fetchOptions(""); }, []); diff --git a/src/components/mui/formik-inputs/mui-formik-datepicker.js b/src/components/mui/formik-inputs/mui-formik-datepicker.js index 3bb67cecc..13f85bcf9 100644 --- a/src/components/mui/formik-inputs/mui-formik-datepicker.js +++ b/src/components/mui/formik-inputs/mui-formik-datepicker.js @@ -5,7 +5,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; import { useField } from "formik"; -const MuiFormikDatepicker = ({ name, label, required }) => { +const MuiFormikDatepicker = ({ name, label, required, ...props }) => { const [field, meta, helpers] = useField(name); const requiredLabel = `${label} *`; return ( @@ -19,8 +19,7 @@ const MuiFormikDatepicker = ({ name, label, required }) => { label: required ? requiredLabel : label, error: meta.touched && Boolean(meta.error), helperText: meta.touched && meta.error, - fullWidth: true, - margin: "normal" + fullWidth: true }, day: { sx: { @@ -36,6 +35,9 @@ const MuiFormikDatepicker = ({ name, label, required }) => { } } }} + margin="normal" + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} /> ); diff --git a/src/components/mui/formik-inputs/mui-formik-radio-group.js b/src/components/mui/formik-inputs/mui-formik-radio-group.js index 01200434f..4c49bb4c3 100644 --- a/src/components/mui/formik-inputs/mui-formik-radio-group.js +++ b/src/components/mui/formik-inputs/mui-formik-radio-group.js @@ -10,13 +10,19 @@ import { } from "@mui/material"; import { useField } from "formik"; -const MuiFormikRadioGroup = ({ name, label, options, ...props }) => { +const MuiFormikRadioGroup = ({ + name, + label, + marginWrapper = "normal", + options, + ...props +}) => { const [field, meta] = useField({ name }); return ( {label && {label}} @@ -56,6 +62,7 @@ const MuiFormikRadioGroup = ({ name, label, options, ...props }) => { MuiFormikRadioGroup.propTypes = { name: PropTypes.string.isRequired, label: PropTypes.string, + marginWrapper: PropTypes.string, options: PropTypes.array.isRequired }; diff --git a/src/components/mui/formik-inputs/mui-formik-select.js b/src/components/mui/formik-inputs/mui-formik-select.js index f8c2aa75e..a6e2aca46 100644 --- a/src/components/mui/formik-inputs/mui-formik-select.js +++ b/src/components/mui/formik-inputs/mui-formik-select.js @@ -5,13 +5,15 @@ import { FormHelperText, FormControl, InputAdornment, - IconButton + IconButton, + InputLabel } from "@mui/material"; import ClearIcon from "@mui/icons-material/Clear"; import { useField } from "formik"; const MuiFormikSelect = ({ name, + label, placeholder, children, isClearable, @@ -24,12 +26,24 @@ const MuiFormikSelect = ({ helpers.setValue(""); }; + const hasValue = field?.value && field.value !== ""; + const shouldShrink = hasValue || Boolean(placeholder); + return ( + {label && ( + + {label} + + )}