diff --git a/package.json b/package.json index 351b69f74..4c24b9f7b 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "react-dropzone": "^4.2.13", "react-final-form": "^6.5.9", "react-google-maps": "^9.4.5", - "react-redux": "^5.0.7", + "react-redux": "^7.1.0", "react-router": "^4.3.1", "react-router-dom": "^4.3.1", "react-rte": "^0.16.3", diff --git a/src/actions/speaker-actions.js b/src/actions/speaker-actions.js index fa8a8b9f9..432dfaaf4 100644 --- a/src/actions/speaker-actions.js +++ b/src/actions/speaker-actions.js @@ -889,19 +889,18 @@ export const getSpeakersBySummit = perPage = DEFAULT_PER_PAGE, order = "full_name", orderDir = DEFAULT_ORDER_DIR, - filters = {} + filters = [] ) => async (dispatch, getState) => { const { currentSummitState } = getState(); const accessToken = await getAccessTokenSafely(); const { currentSummit } = currentSummitState; - const filter = parseFilters(filters); + const filter = [...filters]; dispatch(startLoading()); if (term) { const filterTerm = buildTermFilter(term); - filter.push(filterTerm.join(",")); } diff --git a/src/components/GridFilter/actions/filter-actions.js b/src/components/GridFilter/actions/filter-actions.js new file mode 100644 index 000000000..2d20d6bcf --- /dev/null +++ b/src/components/GridFilter/actions/filter-actions.js @@ -0,0 +1,9 @@ +import { createAction } from "openstack-uicore-foundation/lib/utils/actions"; + +export const SAVE_FILTERS = "SAVE_FILTERS"; + +export const saveFilters = + (id, filters = [], joinOperator = "all") => + (dispatch) => { + dispatch(createAction(SAVE_FILTERS)({ id, filters, joinOperator })); + }; diff --git a/src/components/GridFilter/components/Dropdown.jsx b/src/components/GridFilter/components/Dropdown.jsx new file mode 100644 index 000000000..fbd66e184 --- /dev/null +++ b/src/components/GridFilter/components/Dropdown.jsx @@ -0,0 +1,83 @@ +/** + * Copyright 2026 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 T from "i18n-react/dist/i18n-react"; +import { Select, FormControl, MenuItem, InputLabel } from "@mui/material"; +import PropTypes from "prop-types"; + +const Dropdown = ({ + id, + value, + options, + placeholder, + label, + onChange, + ...rest +}) => { + const finalPlaceholder = + placeholder || T.translate("general.select_an_option"); + + return ( + + {label && {label}} + + + ); +}; + +Dropdown.propTypes = { + id: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + .isRequired, + label: PropTypes.string.isRequired + }) + ).isRequired, + label: PropTypes.string, + placeholder: PropTypes.string, + onChange: PropTypes.func.isRequired +}; + +Dropdown.defaultProps = { + value: null, + label: "", + placeholder: "" +}; + +export default Dropdown; diff --git a/src/components/GridFilter/components/Filter.jsx b/src/components/GridFilter/components/Filter.jsx new file mode 100644 index 000000000..3674e0774 --- /dev/null +++ b/src/components/GridFilter/components/Filter.jsx @@ -0,0 +1,145 @@ +/** + * Copyright 2026 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 T from "i18n-react/dist/i18n-react"; +import { Box, Grid2, IconButton } from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import AddIcon from "@mui/icons-material/Add"; +import PropTypes from "prop-types"; +import Dropdown from "./Dropdown"; +import ValueInput from "./ValueInput"; +import RoundButton from "./RoundButton"; + +const Filter = ({ id, value, criterias, onChange, onAdd, onDelete }) => { + const criteriaOptions = criterias.map(({ key, label }) => ({ + value: key, + label + })); + const criteriaObj = criterias.find(({ key }) => key === value?.criteria); + const operatorOptions = criteriaObj?.operators || []; + const valueSettings = criteriaObj?.values || {}; + + const handleChange = (prop, val) => { + onChange({ ...value, [prop]: val }); + }; + + const handleChangeCriteria = (ev) => { + const val = ev.target.value; + handleChange("criteria", val); + }; + + const handleChangeOperator = (ev) => { + const val = ev.target.value; + handleChange("operator", val); + }; + + const handleChangeValue = (ev) => { + const val = ev.target.value; + handleChange("value", val); + }; + + return ( + + + + + + + + + + {value?.id !== "new" ? ( + onDelete(value)} + size="large" + > + + + ) : ( + onAdd()} + disabled={!value?.criteria || !value?.operator || !value?.value} + sx={{ ml: "4px" }} + > + + + )} + + + ); +}; + +Filter.propTypes = { + id: PropTypes.string.isRequired, + value: PropTypes.shape({ + criteria: PropTypes.string, + operator: PropTypes.string, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.bool, + PropTypes.array + ]) + }), + criterias: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + operators: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired + }) + ), + values: PropTypes.shape({ + type: PropTypes.string.isRequired, + props: PropTypes.object.isRequired + }) + }) + ).isRequired, + onChange: PropTypes.func.isRequired, + onAdd: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired +}; + +Filter.defaultProps = { + value: null +}; + +export default Filter; diff --git a/src/components/GridFilter/components/FilterButton.jsx b/src/components/GridFilter/components/FilterButton.jsx new file mode 100644 index 000000000..67fdf2190 --- /dev/null +++ b/src/components/GridFilter/components/FilterButton.jsx @@ -0,0 +1,59 @@ +/** + * Copyright 2026 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 PropTypes from "prop-types"; +import { Chip, IconButton } from "@mui/material"; +import FilterListIcon from "@mui/icons-material/FilterList"; +import T from "i18n-react/dist/i18n-react"; + +const FilterButton = ({ filterCount, onClick, onDelete }) => { + if (filterCount > 0) { + return ( + } + label={`${filterCount} ${T.translate("grid_filter.filters")}`} + onClick={onClick} + onDelete={onDelete} + sx={{ + "& .MuiChip-label": { fontSize: "13px" }, + backgroundColor: "grey.700", + color: "white", + "& .MuiChip-icon": { color: "white" }, + "& .MuiChip-deleteIcon": { + color: "rgba(255,255,255,0.7)", + "&:hover": { color: "white" } + } + }} + /> + ); + } + + return ( + + + + ); +}; + +FilterButton.propTypes = { + filterCount: PropTypes.number.isRequired, + onClick: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired +}; + +export default FilterButton; diff --git a/src/components/GridFilter/components/RoundButton.jsx b/src/components/GridFilter/components/RoundButton.jsx new file mode 100644 index 000000000..887595059 --- /dev/null +++ b/src/components/GridFilter/components/RoundButton.jsx @@ -0,0 +1,44 @@ +/** + * Copyright 2026 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 { Button } from "@mui/material"; +import PropTypes from "prop-types"; + +const RoundButton = ({ children, sx = {}, ...props }) => ( + +); + +RoundButton.propTypes = { + children: PropTypes.node.isRequired, + sx: PropTypes.object +}; + +RoundButton.defaultProps = { + sx: {} +}; + +export default RoundButton; diff --git a/src/components/GridFilter/components/ToggleButtons.jsx b/src/components/GridFilter/components/ToggleButtons.jsx new file mode 100644 index 000000000..542a3a580 --- /dev/null +++ b/src/components/GridFilter/components/ToggleButtons.jsx @@ -0,0 +1,65 @@ +/** + * Copyright 2026 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 { ToggleButtonGroup, ToggleButton } from "@mui/material"; +import PropTypes from "prop-types"; + +const ToggleButtons = ({ options, value, onChange, color = "primary" }) => ( + { + if (newValue !== null) onChange(newValue); + }} + sx={(theme) => { + const theColor = theme.palette[color]?.main ?? theme.palette.primary.main; + return { + border: `1px solid ${theColor}`, + overflow: "hidden", + "& .MuiToggleButtonGroup-grouped": { + color: theColor, + fontSize: "14px", + padding: "2px 16px", + "&.Mui-selected": { + backgroundColor: theColor, + color: "#fff", + "&:hover": { backgroundColor: theColor } + }, + "&:hover": { backgroundColor: `${theColor}18` } + } + }; + }} + > + {options.map((option) => ( + + {option} + + ))} + +); + +ToggleButtons.propTypes = { + options: PropTypes.arrayOf(PropTypes.string).isRequired, + value: PropTypes.string, + color: PropTypes.string, + onChange: PropTypes.func.isRequired +}; + +ToggleButtons.defaultProps = { + value: null, + color: "primary" +}; + +export default ToggleButtons; diff --git a/src/components/GridFilter/components/ValueInput/index.jsx b/src/components/GridFilter/components/ValueInput/index.jsx new file mode 100644 index 000000000..5c457e2fb --- /dev/null +++ b/src/components/GridFilter/components/ValueInput/index.jsx @@ -0,0 +1,54 @@ +/** + * Copyright 2026 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 TextField from "@mui/material/TextField"; +import PropTypes from "prop-types"; +import Dropdown from "../Dropdown"; + +const INPUT_TYPE_MAP = { text: TextField, select: Dropdown }; + +const ValueInput = ({ type, ...rest }) => { + const Component = type ? INPUT_TYPE_MAP[type] : Dropdown; // use dropdown as a placeholder + // eslint-disable-next-line react/jsx-props-no-spreading + return Component ? : null; +}; + +ValueInput.propTypes = { + id: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.array + ]), + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + .isRequired, + label: PropTypes.string.isRequired + }) + ), + label: PropTypes.string, + placeholder: PropTypes.string, + onChange: PropTypes.func.isRequired +}; + +ValueInput.defaultProps = { + value: null, + label: "", + placeholder: "", + options: null +}; + +export default ValueInput; diff --git a/src/components/GridFilter/hooks/useGridFilter.jsx b/src/components/GridFilter/hooks/useGridFilter.jsx new file mode 100644 index 000000000..cc3c97912 --- /dev/null +++ b/src/components/GridFilter/hooks/useGridFilter.jsx @@ -0,0 +1,31 @@ +import { useSelector } from "react-redux"; + +export const EMPTY_FILTER = { + criteria: null, + operator: null, + value: null, + id: "new" +}; + +const useGridFilter = (id) => { + const allFilters = useSelector( + (state) => state.allGridFiltersState.allFilters + ); + const filter = allFilters.find((f) => f.id === id) || {}; + const { filterValues = [], joinOperator = "all", parsedFilter = [] } = filter; + + const valuesWithIds = filterValues.map((v, i) => ({ + ...v, + id: `${v.criteria}-${i}` + })); + + return { + filterValues, + filterCount: filterValues.length, + joinOperator, + parsedFilter, + valuesWithIds + }; +}; + +export default useGridFilter; diff --git a/src/components/GridFilter/index.jsx b/src/components/GridFilter/index.jsx new file mode 100644 index 000000000..21f7ca57d --- /dev/null +++ b/src/components/GridFilter/index.jsx @@ -0,0 +1,292 @@ +/** + * Copyright 2026 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, useMemo, useState } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import T from "i18n-react/dist/i18n-react"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + Divider, + Typography +} from "@mui/material"; +import ToggleButtons from "./components/ToggleButtons"; +import Filter from "./components/Filter"; +import FilterButton from "./components/FilterButton"; +import { saveFilters } from "./actions/filter-actions"; +import useGridFilter, { EMPTY_FILTER } from "./hooks/useGridFilter"; +import { JOIN_OPERATORS } from "./utils"; + +// sample props +/* +criterias = [ + { + key: "tracks", + label: "Tracks", + operators: [ + {value: "==", label: "is"}, + {value: "=@", label: "like"}, + ], + values: { + type: "select", + props: { + options: [ + {value: 1, label: "OpenStack"}, + {value: 2, label: "FnTech"} + ], + multi: true, + placeholder: "Select Tracks" + }, + }, + }, + { + key: "selection_status", + label: "Selection Status", + operators: [{ value: "==", label: "is" }], + values: { + type: "select", + props: { + options: [...selectionStatusOptions], + placeholder: "Filter by Selection Status" + } + }, + customParser: (f) => { + const filter = []; + if (f.value) { + switch (f.value) { + case "only_rejected": + filter.push("has_rejected_presentations==true"); + filter.push("has_accepted_presentations==false"); + filter.push("has_alternate_presentations==false"); + break; + case "only_accepted": + filter.push("has_rejected_presentations==false"); + filter.push("has_accepted_presentations==true"); + filter.push("has_alternate_presentations==false"); + break; + case "only_alternate": + filter.push("has_rejected_presentations==false"); + filter.push("has_accepted_presentations==false"); + filter.push("has_alternate_presentations==true"); + break; + } + } + return filter; + }, + }, + { + key: "sponsor", + label: "Sponsor", + operators: [ + {value: "==", label: "is"}, + {value: "=@", label: "like"}, + ], + values: { + type: "text", + props: { + placeholder: "Type Sponsor Name" + }, + }, + } + ] + + +value = [ + { + criteria: "tracks", + operator: "==", + value: [1, 2] + }, + { + criteria: "sponsor", + operator: "=@", + value: "openstack" + } +] + */ + +const GridFilter = ({ id, criterias, onApply, saveFilters }) => { + const { joinOperator, filterCount, valuesWithIds } = useGridFilter(id); + const valuesString = useMemo( + () => valuesWithIds.map((v) => v.id).join(","), + [valuesWithIds] + ); + const [openModal, setOpenModal] = useState(false); + const [filters, setFilters] = useState([]); + const [andOrAny, setAndOrAny] = useState(joinOperator); + + useEffect(() => { + if (openModal) { + // we want to rest to applied filters when closing modal (Cancel) + setFilters([...valuesWithIds, EMPTY_FILTER]); + setAndOrAny(joinOperator); + } + }, [valuesString, joinOperator, openModal]); + + const parseFilter = (filter) => { + const parser = criterias.find( + ({ key }) => key === filter.criteria + )?.customParser; + if (parser) { + return parser(filter); + } + const value = Array.isArray(filter.value) + ? filter.value.join("||") + : filter.value; + if (value) { + return [`${filter.criteria}${filter.operator}${value}`]; + } + }; + + const handleChange = (filter) => { + setFilters((prevFilters) => + prevFilters.map((f) => (f.id === filter.id ? filter : f)) + ); + }; + + const handleAdd = () => { + setFilters((prevFilters) => { + // replacing "new" id and adding new empty filter + const currentFilters = prevFilters.map((f, i) => ({ + ...f, + id: `${f.criteria}-${i}` + })); + return [...currentFilters, EMPTY_FILTER]; + }); + }; + + const handleRemove = (filter) => { + setFilters((prevFilters) => prevFilters.filter((f) => f.id !== filter.id)); + }; + + const handleClear = () => { + setFilters([EMPTY_FILTER]); + }; + + const handleSubmit = () => { + // remove empty filters and adding parsed string for API + const validFilters = filters + .filter((f) => f.criteria && f.operator && f.value) + .map((f) => ({ ...f, parsed: parseFilter(f) })); + + saveFilters(id, validFilters, andOrAny); + onApply(validFilters, andOrAny); + setOpenModal(false); + }; + + const handleRemoveAndApply = () => { + saveFilters(id); + onApply([], JOIN_OPERATORS.ALL); + }; + + return ( + <> + setOpenModal(true)} + onDelete={handleRemoveAndApply} + /> + setOpenModal(false)} + maxWidth="md" + fullWidth + > + + + + {T.translate("grid_filter.filter_by")} + + + + {T.translate("grid_filter.following")} + + + + + {filters.map((filter) => ( + + ))} + + + + + + + + + + + ); +}; + +GridFilter.propTypes = { + id: PropTypes.string.isRequired, + criterias: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + operators: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired + }) + ), + values: PropTypes.shape({ + type: PropTypes.string.isRequired, + props: PropTypes.object.isRequired + }) + }) + ).isRequired, + onApply: PropTypes.func, + saveFilters: PropTypes.func.isRequired +}; + +GridFilter.defaultProps = { + onApply: () => {} +}; + +export default connect(null, { saveFilters })(GridFilter); diff --git a/src/components/GridFilter/readme.md b/src/components/GridFilter/readme.md new file mode 100644 index 000000000..ad5dbe128 --- /dev/null +++ b/src/components/GridFilter/readme.md @@ -0,0 +1,6 @@ +## GRID FILTER + +# set up + +- Need to add the all-filters-reducer to the host app store +- Then access the allFilters state from it with reducer connect and filterBy key diff --git a/src/components/GridFilter/reducers/all-filters-reducer.js b/src/components/GridFilter/reducers/all-filters-reducer.js new file mode 100644 index 000000000..dacdbf346 --- /dev/null +++ b/src/components/GridFilter/reducers/all-filters-reducer.js @@ -0,0 +1,43 @@ +import { LOGOUT_USER } from "openstack-uicore-foundation/lib/security/actions"; +import filterReducer from "./filter-reducer"; +import { SAVE_FILTERS } from "../actions/filter-actions"; + +const DEFAULT_STATE = { + allFilters: [] +}; + +const allFiltersReducer = (state = DEFAULT_STATE, action) => { + const { type, payload } = action; + + switch (type) { + case LOGOUT_USER: + return DEFAULT_STATE; + + case SAVE_FILTERS: { + const { id } = payload; + const { allFilters } = state; + const filterExists = allFilters.find((f) => f.id === id); + let newFilters; + + if (filterExists) { + newFilters = allFilters.map((f) => { + if (f.id === id) { + return filterReducer(f, { ...action, type: `FIL_${type}` }); + } + return f; + }); + } else { + newFilters = [ + ...allFilters, + filterReducer(null, { ...action, type: `FIL_${type}` }) + ]; + } + + return { ...state, allFilters: newFilters }; + } + default: + return state; + } +}; + +export default allFiltersReducer; diff --git a/src/components/GridFilter/reducers/filter-reducer.js b/src/components/GridFilter/reducers/filter-reducer.js new file mode 100644 index 000000000..a174d393f --- /dev/null +++ b/src/components/GridFilter/reducers/filter-reducer.js @@ -0,0 +1,33 @@ +import { SAVE_FILTERS } from "../actions/filter-actions"; +import { JOIN_OPERATORS } from "../utils"; + +const INITIAL_STATE = { + id: null, + joinOperator: "all", + filterValues: [], + parsedFilter: [] +}; + +const filterReducer = (state = INITIAL_STATE, action) => { + const { type, payload } = action; + + switch (type) { + case `FIL_${SAVE_FILTERS}`: { + const { id, filters, joinOperator } = payload; + let parsedFilter = filters.flatMap((f) => f.parsed); + if (joinOperator === JOIN_OPERATORS.ANY) + parsedFilter = parsedFilter.map((p) => `or(${p})`); + return { + ...state, + id, + filterValues: filters, + joinOperator, + parsedFilter + }; + } + default: + return state; + } +}; + +export default filterReducer; diff --git a/src/components/GridFilter/utils.js b/src/components/GridFilter/utils.js new file mode 100644 index 000000000..e2f1db30c --- /dev/null +++ b/src/components/GridFilter/utils.js @@ -0,0 +1,16 @@ +export const OPERATORS = { + IS: { value: "==", label: "is" }, + LIKE: { value: "=@", label: "like" }, + LIKE_START: { value: "@@", label: "like start" }, + IS_NOT: { value: "<>", label: "is not" }, + HAS: { value: ">>", label: "has" }, + HAS_NOT: { value: "!>>", label: "has not" }, + LESS: { value: "<", label: "less than" }, + LESS_OR_EQUAL: { value: "<=", label: "less than or equal to" }, + GREATER: { value: ">", label: "greater than" }, + GREATER_OR_EQUAL: { value: ">=", label: "greater than or equal to" }, + BETWEEN: { value: "[]", label: "between" }, + BETWEEN_STRICT: { value: "()", label: "between strict" } +}; + +export const JOIN_OPERATORS = { ALL: "all", ANY: "any" }; diff --git a/src/i18n/en.json b/src/i18n/en.json index 9c4f9fd05..d7ddf9f46 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -4117,5 +4117,16 @@ "resync_helper": "Use the sync icon to re-sync individual room folders to Dropbox.", "resync_tooltip": "Re-sync Dropbox folder", "resync_dispatched": "Room re-sync task has been dispatched." + }, + "grid_filter": { + "filter_by": "Filter by ", + "following": " one of the following: ", + "select_criteria": "Select criteria", + "select_operator": "Select operator", + "select_values": "Select values", + "filters": "Filters", + "clear_filters": "Clear Filters", + "cancel": "Cancel", + "apply_filters": "Apply Filters" } } diff --git a/src/pages/summit_speakers/summit-speakers-list-page.js b/src/pages/summit_speakers/summit-speakers-list-page.js deleted file mode 100644 index 24e47df74..000000000 --- a/src/pages/summit_speakers/summit-speakers-list-page.js +++ /dev/null @@ -1,1225 +0,0 @@ -/** - * Copyright 2017 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 { connect } from "react-redux"; -import T from "i18n-react/dist/i18n-react"; -import Swal from "sweetalert2"; -import { Modal, Pagination } from "react-bootstrap"; -import FreeTextSearch from "openstack-uicore-foundation/lib/components/free-text-search" -import SelectableTable from "openstack-uicore-foundation/lib/components/table-selectable" -import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown" -import Input from "openstack-uicore-foundation/lib/components/inputs/text-input"; -import SpeakerPromoCodeSpecForm from "../../components/forms/speakers-promo-code-spec-form"; -import { - initSpeakersList, - getSpeakersBySummit, - exportSummitSpeakers, - selectSummitSpeaker, - unselectSummitSpeaker, - selectAllSummitSpeakers, - unselectAllSummitSpeakers, - setCurrentFlowEvent, - sendSpeakerEmails -} from "../../actions/speaker-actions"; -import { - initSubmittersList, - getSubmittersBySummit, - exportSummitSubmitters, - selectSummitSubmitter, - unselectSummitSubmitter, - selectAllSummitSubmitters, - unselectAllSummitSubmitters, - setCurrentSubmitterFlowEvent, - sendSubmitterEmails -} from "../../actions/submitter-actions"; -import { - validateSpecs, - resetPromoCodeSpecForm -} from "../../actions/promocode-specification-actions"; -import { - EXISTING_SPEAKERS_PROMO_CODE, - EXISTING_SPEAKERS_DISCOUNT_CODE, - AUTO_GENERATED_SPEAKERS_PROMO_CODE, - AUTO_GENERATED_SPEAKERS_DISCOUNT_CODE -} from "../../actions/promocode-actions"; - -import { ALL_FILTER, SpeakersSources as sources } from "../../utils/constants"; -import { validateEmail } from "../../utils/methods"; -import MediaTypeFilter from "../../components/filters/media-type-filter"; - -import "../../styles/speakers-list-page.less"; - -class SummitSpeakersListPage extends React.Component { - constructor(props) { - super(props); - - this.getSubjectProps = this.getSubjectProps.bind(this); - this.export = this.export.bind(this); - this.getBySummit = this.getBySummit.bind(this); - this.handleSpeakerSubmitterSourceChange = - this.handleSpeakerSubmitterSourceChange.bind(this); - this.handleEdit = this.handleEdit.bind(this); - this.handlePageChange = this.handlePageChange.bind(this); - this.handleSort = this.handleSort.bind(this); - this.handleSearch = this.handleSearch.bind(this); - this.handleExport = this.handleExport.bind(this); - this.handleSelected = this.handleSelected.bind(this); - this.handleSelectedAll = this.handleSelectedAll.bind(this); - this.handleChangeSelectionPlanFilter = - this.handleChangeSelectionPlanFilter.bind(this); - this.handleChangeTrackFilter = this.handleChangeTrackFilter.bind(this); - this.handleChangeTrackGroupFilter = - this.handleChangeTrackGroupFilter.bind(this); - this.handleChangeActivityTypeFilter = - this.handleChangeActivityTypeFilter.bind(this); - this.handleChangeSelectionStatusFilter = - this.handleChangeSelectionStatusFilter.bind(this); - this.handleChangeFlowEvent = this.handleChangeFlowEvent.bind(this); - this.showEmailSendModal = this.showEmailSendModal.bind(this); - this.handleSendEmails = this.handleSendEmails.bind(this); - this.handleChangePromoCodeStrategy = - this.handleChangePromoCodeStrategy.bind(this); - this.handleOrAndFilter = this.handleOrAndFilter.bind(this); - this.handleChangeMediaUploadTypeFilter = - this.handleChangeMediaUploadTypeFilter.bind(this); - - this.state = { - testRecipient: "", - showSendEmailModal: false, - excerptRecipient: "", - source: sources.speakers, - promoCodeStrategy: 0, - speakerFilters: { - orAndFilter: ALL_FILTER - } - }; - } - - componentDidMount() { - const { currentSummit, initSubmittersList, initSpeakersList } = this.props; - initSubmittersList(); - initSpeakersList(); - if (currentSummit) { - const { - term, - page, - order, - orderDir, - perPage, - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - const { - speakerFilters: { orAndFilter } - } = this.state; - this.getBySummit(term, page, perPage, order, orderDir, { - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - orAndFilter, - mediaUploadTypeFilter - }); - } - } - - getSubjectProps() { - const { source } = this.state; - return source === sources.speakers - ? this.props.speakersProps - : this.props.submittersProps; - } - - getBySummit(term, page, perPage, order, orderDir, filters) { - const { source } = this.state; - const callable = - source === sources.speakers - ? this.props.getSpeakersBySummit - : this.props.getSubmittersBySummit; - callable(term, page, perPage, order, orderDir, filters, source); - } - - export(term, order, orderDir, filters) { - const { source } = this.state; - const callable = - source === sources.speakers - ? this.props.exportSummitSpeakers - : this.props.exportSummitSubmitters; - callable(term, order, orderDir, filters, source); - } - - handleSpeakerSubmitterSourceChange(ev) { - const { value } = ev.target; - const { - term, - order, - orderDir, - perPage, - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - const { - speakerFilters: { orAndFilter } - } = this.state; - const { initSubmittersList, initSpeakersList } = this.props; - this.setState({ ...this.state, source: value }, function () { - initSubmittersList(); - initSpeakersList(); - this.getBySummit(term, 1, perPage, order, orderDir, { - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - orAndFilter, - mediaUploadTypeFilter - }); - }); - } - - handleEdit(itemId) { - if (this.state.source === sources.speakers) { - const { history } = this.props; - history.push(`/app/speakers/${itemId}`); - } - } - - handlePageChange(page) { - const { - term, - order, - orderDir, - perPage, - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - const { - speakerFilters: { orAndFilter } - } = this.state; - this.getBySummit(term, page, perPage, order, orderDir, { - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - orAndFilter, - mediaUploadTypeFilter - }); - } - - handleSort(index, key, dir) { - const { - term, - page, - perPage, - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - const { - speakerFilters: { orAndFilter } - } = this.state; - this.getBySummit(term, page, perPage, key, dir, { - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - orAndFilter, - mediaUploadTypeFilter - }); - } - - handleSearch(term) { - const { - order, - orderDir, - page, - perPage, - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - const { - speakerFilters: { orAndFilter } - } = this.state; - this.getBySummit(term, page, perPage, order, orderDir, { - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - orAndFilter, - mediaUploadTypeFilter - }); - } - - handleChangeSelectionPlanFilter(ev) { - const { value: newSelectionPlanFilter } = ev.target; - const { - term, - order, - page, - orderDir, - perPage, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - const { - speakerFilters: { orAndFilter } - } = this.state; - this.getBySummit(term, page, perPage, order, orderDir, { - selectionPlanFilter: newSelectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - orAndFilter, - mediaUploadTypeFilter - }); - } - - handleChangeTrackFilter(ev) { - const { value: newTrackFilter } = ev.target; - const { - term, - order, - page, - orderDir, - perPage, - selectionPlanFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - const { - speakerFilters: { orAndFilter } - } = this.state; - this.getBySummit(term, page, perPage, order, orderDir, { - selectionPlanFilter, - trackFilter: newTrackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - orAndFilter, - mediaUploadTypeFilter - }); - } - - handleChangeTrackGroupFilter(ev) { - const { value: newTrackGroupFilter } = ev.target; - const { - term, - order, - page, - orderDir, - perPage, - selectionPlanFilter, - trackFilter, - activityTypeFilter, - selectionStatusFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - const { - speakerFilters: { orAndFilter } - } = this.state; - this.getBySummit(term, page, perPage, order, orderDir, { - selectionPlanFilter, - trackFilter, - trackGroupFilter: newTrackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - orAndFilter, - mediaUploadTypeFilter - }); - } - - handleChangeActivityTypeFilter(ev) { - const { value: newActivityTypeFilter } = ev.target; - const { - term, - order, - page, - orderDir, - perPage, - selectionPlanFilter, - trackFilter, - trackGroupFilter, - selectionStatusFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - const { - speakerFilters: { orAndFilter } - } = this.state; - this.getBySummit(term, page, perPage, order, orderDir, { - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter: newActivityTypeFilter, - selectionStatusFilter, - orAndFilter, - mediaUploadTypeFilter - }); - } - - handleChangeMediaUploadTypeFilter(ev) { - const { value, operator } = ev.target; - const { - term, - order, - page, - orderDir, - perPage, - activityTypeFilter, - selectionPlanFilter, - trackFilter, - trackGroupFilter, - selectionStatusFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - const { - speakerFilters: { orAndFilter } - } = this.state; - if (operator && value.length > 0) { - this.getBySummit(term, page, perPage, order, orderDir, { - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - orAndFilter, - mediaUploadTypeFilter: { operator, value } - }); - // get speakers if the media upload types filter is clear - } else if (mediaUploadTypeFilter.value.length > 0 && value.length === 0) { - this.getBySummit(term, page, perPage, order, orderDir, { - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - orAndFilter, - mediaUploadTypeFilter: { operator: null, value: [] } - }); - } - } - - handleChangeSelectionStatusFilter(ev) { - let { value: newSelectionStatusFilter } = ev.target; - // exclusive filters tests .... - if (newSelectionStatusFilter.includes("only_rejected")) { - newSelectionStatusFilter = ["only_rejected"]; - } else if (newSelectionStatusFilter.includes("only_alternate")) { - newSelectionStatusFilter = ["only_alternate"]; - } else if (newSelectionStatusFilter.includes("only_accepted")) { - newSelectionStatusFilter = ["only_accepted"]; - } else if (newSelectionStatusFilter.includes("accepted_alternate")) { - newSelectionStatusFilter = ["accepted_alternate"]; - } else if (newSelectionStatusFilter.includes("accepted_rejected")) { - newSelectionStatusFilter = ["accepted_rejected"]; - } else if (newSelectionStatusFilter.includes("alternate_rejected")) { - newSelectionStatusFilter = ["alternate_rejected"]; - } - - const { - term, - order, - page, - orderDir, - perPage, - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - const { - speakerFilters: { orAndFilter } - } = this.state; - this.getBySummit(term, page, perPage, order, orderDir, { - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - orAndFilter, - selectionStatusFilter: newSelectionStatusFilter, - mediaUploadTypeFilter - }); - } - - handleChangeFlowEvent(ev) { - const { value } = ev.target; - const { source } = this.state; - if (source === sources.speakers) { - this.props.setCurrentFlowEvent(value); - } else { - this.props.setCurrentSubmitterFlowEvent(value); - } - } - - handleSendEmails(ev) { - ev.stopPropagation(); - ev.preventDefault(); - const { currentPromocodeSpecification } = this.props; - const { promoCodeStrategy, testRecipient, source } = this.state; - const isSpeakerMode = source === sources.speakers; - const excerptRecipient = this.ingestEmailRef.value; - const shouldSendCopy2Submitter = - isSpeakerMode && this.shouldSendCopy2SubmitterRef.checked; - const { - term, - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - const { - speakerFilters: { orAndFilter } - } = this.state; - - this.props.validateSpecs( - promoCodeStrategy, - currentPromocodeSpecification.entity, - () => { - this.setState({ - showSendEmailModal: false, - excerptRecipient: "", - testRecipient: "", - promoCodeStrategy: 0 - }); - // send emails - - const callable = isSpeakerMode - ? this.props.sendSpeakerEmails - : this.props.sendSubmitterEmails; - - callable( - term, - { - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - orAndFilter, - mediaUploadTypeFilter - }, - testRecipient, - excerptRecipient, - shouldSendCopy2Submitter, - source, - promoCodeStrategy, - currentPromocodeSpecification.entity - ); - } - ); - } - - handleChangePromoCodeStrategy(ev) { - const { value } = ev.target; - this.setState({ ...this.state, promoCodeStrategy: value }); - this.props.resetPromoCodeSpecForm(); - } - - showEmailSendModal(ev) { - ev.stopPropagation(); - ev.preventDefault(); - - const { source, testRecipient } = this.state; - const { currentFlowEvent, selectedCount } = this.getSubjectProps(); - - if (!currentFlowEvent) { - Swal.fire( - "Validation error", - T.translate("summit_speakers_list.select_template"), - "warning" - ); - return false; - } - - if (selectedCount === 0) { - const content = - source === sources.speakers - ? T.translate("summit_speakers_list.select_items") - : T.translate("summit_submitters_list.select_items"); - Swal.fire("Validation error", content, "warning"); - return false; - } - - if (testRecipient !== "" && !validateEmail(testRecipient)) { - Swal.fire( - "Validation error", - T.translate("summit_speakers_list.invalid_recipient_email"), - "warning" - ); - return false; - } - - this.setState({ - ...this.state, - showSendEmailModal: true, - excerptRecipient: "" - }); - } - - handleExport(ev) { - const { - term, - order, - orderDir, - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - const { - speakerFilters: { orAndFilter } - } = this.state; - ev.preventDefault(); - this.export(term, order, orderDir, { - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - orAndFilter, - mediaUploadTypeFilter - }); - } - - handleSelected(item_id, isSelected) { - const { source } = this.state; - if (isSelected) { - if (source === sources.speakers) { - this.props.selectSummitSpeaker(item_id); - } else { - this.props.selectSummitSubmitter(item_id); - } - return; - } - if (source === sources.speakers) { - this.props.unselectSummitSpeaker(item_id); - } else { - this.props.unselectSummitSubmitter(item_id); - } - } - - handleSelectedAll(ev) { - const selectedAll = ev.target.checked; - const { source } = this.state; - if (source === sources.speakers) { - this.props.selectAllSummitSpeakers(); - } else { - this.props.selectAllSummitSubmitters(); - } - if (!selectedAll) { - // clear all selected - if (source === sources.speakers) { - this.props.unselectAllSummitSpeakers(); - } else { - this.props.unselectAllSummitSubmitters(); - } - } - } - - handleOrAndFilter(ev) { - const { - term, - order, - page, - orderDir, - perPage, - trackFilter, - trackGroupFilter, - selectionPlanFilter, - activityTypeFilter, - selectionStatusFilter, - mediaUploadTypeFilter - } = this.getSubjectProps(); - this.setState({ - ...this.state, - speakerFilters: { ...this.state.speakerFilters, orAndFilter: ev } - }); - this.getBySummit(term, page, perPage, order, orderDir, { - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - mediaUploadTypeFilter, - selectionStatusFilter, - orAndFilter: ev - }); - } - - render() { - const { currentSummit, currentPromocodeSpecification } = this.props; - - const { testRecipient, source, promoCodeStrategy } = this.state; - - const { - items, - lastPage, - currentPage, - term, - order, - orderDir, - totalItems, - selectedCount, - selectedAll, - selectionPlanFilter, - trackFilter, - trackGroupFilter, - activityTypeFilter, - selectionStatusFilter, - mediaUploadTypeFilter, - currentFlowEvent - } = this.getSubjectProps(); - - const columns = [ - { - columnKey: "full_name", - value: T.translate("general.name"), - sortable: true - }, - { - columnKey: "email", - value: T.translate("general.email"), - sortable: true - }, - { - columnKey: "accepted_presentations_count", - value: T.translate("summit_speakers_list.accepted") - }, - { - columnKey: "alternate_presentations_count", - value: T.translate("summit_speakers_list.alternate") - }, - { - columnKey: "rejected_presentations_count", - value: T.translate("summit_speakers_list.rejected") - } - ]; - - const selectionPlansDDL = currentSummit.selection_plans.map( - (selectionPlan) => ({ - label: selectionPlan.name, - value: selectionPlan.id - }) - ); - const tracksDDL = currentSummit.tracks.map((track) => ({ - label: track.name, - value: track.id - })); - const trackGroupsDDL = currentSummit.track_groups.map((trackGroup) => ({ - label: trackGroup.name, - value: trackGroup.id - })); - const activityTypesDDL = currentSummit.event_types.map((type) => ({ - label: type.name, - value: type.id - })); - - const selectionStatusDDL = [ - { label: "Accepted", value: "accepted" }, - { label: "Alternate", value: "alternate" }, - { label: "Rejected", value: "rejected" }, - { label: "Only Rejected", value: "only_rejected" }, - { label: "Only Accepted", value: "only_accepted" }, - { label: "Only Alternate", value: "only_alternate" }, - { label: "Accepted/Alternate", value: "accepted_alternate" }, - { label: "Accepted/Rejected", value: "accepted_rejected" }, - { label: "Alternate/Rejected", value: "alternate_rejected" } - ]; - - const speakerSubmitterSourceSelectorDDL = [ - { - label: T.translate("summit_speakers_list.speakers"), - value: sources.speakers - }, - { - label: T.translate("summit_submitters_list.submitters"), - value: sources.submitters - }, - { - label: T.translate("summit_submitters_list.submitters_no_speakers"), - value: sources.submitters_no_speakers - } - ]; - - const emailFlowDDL = - this.state.source === sources.speakers - ? [ - { label: "-- SELECT EMAIL EVENT --", value: "" }, - { - label: - "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ACCEPTED_ALTERNATE", - value: - "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ACCEPTED_ALTERNATE" - }, - { - label: - "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ACCEPTED_REJECTED", - value: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ACCEPTED_REJECTED" - }, - { - label: - "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ALTERNATE_REJECTED", - value: - "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ALTERNATE_REJECTED" - }, - { - label: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ACCEPTED_ONLY", - value: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ACCEPTED_ONLY" - }, - { - label: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ALTERNATE_ONLY", - value: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ALTERNATE_ONLY" - }, - { - label: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_REJECTED_ONLY", - value: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_REJECTED_ONLY" - } - ] - : [ - { label: "-- SELECT EMAIL EVENT --", value: "" }, - { - label: - "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ACCEPTED_ALTERNATE", - value: - "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ACCEPTED_ALTERNATE" - }, - { - label: - "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ACCEPTED_REJECTED", - value: - "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ACCEPTED_REJECTED" - }, - { - label: - "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ALTERNATE_REJECTED", - value: - "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ALTERNATE_REJECTED" - }, - { - label: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ACCEPTED_ONLY", - value: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ACCEPTED_ONLY" - }, - { - label: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ALTERNATE_ONLY", - value: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ALTERNATE_ONLY" - }, - { - label: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_REJECTED_ONLY", - value: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_REJECTED_ONLY" - } - ]; - - const promoCodeStrategiesDDL = [ - { - label: T.translate("summit_speakers_list.select_promo_code_strategy"), - value: 0 - }, - { - label: T.translate("summit_speakers_list.select_speaker_promo_code"), - value: EXISTING_SPEAKERS_PROMO_CODE - }, - { - label: T.translate("summit_speakers_list.select_speaker_discount_code"), - value: EXISTING_SPEAKERS_DISCOUNT_CODE - }, - { - label: T.translate( - "summit_speakers_list.select_auto_generate_speaker_promo_code" - ), - value: AUTO_GENERATED_SPEAKERS_PROMO_CODE - }, - { - label: T.translate( - "summit_speakers_list.select_auto_generate_speaker_discount_code" - ), - value: AUTO_GENERATED_SPEAKERS_DISCOUNT_CODE - } - ]; - - const table_options = { - sortCol: order, - sortDir: orderDir, - actions: { - edit: { - onClick: this.handleEdit, - onSelected: this.handleSelected, - onSelectedAll: this.handleSelectedAll - } - }, - selectedAll - }; - - if (!currentSummit.id) return
; - - return ( -
-

- {" "} - {this.state.source === sources.speakers - ? T.translate("summit_speakers_list.summit_speakers_list") - : T.translate("summit_submitters_list.summit_submitters_list")}{" "} - ({totalItems}) -

-
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
- -
-
- - this.setState({ testRecipient: ev.target.value }) - } - placeholder={T.translate( - "summit_speakers_list.placeholders.test_recipient" - )} - /> -
-
- -
-
- - {items.length === 0 && ( -
- {this.state.source === sources.speakers - ? T.translate("summit_speakers_list.no_speakers") - : T.translate("summit_submitters_list.no_submitters")} -
- )} - - {items.length > 0 && ( -
- - - {T.translate("summit_speakers_list.items_qty", { - qty: selectedCount - })} - - - - - - - this.setState({ ...this.state, showSendEmailModal: false }) - } - backdrop={false} - > - - - {this.state.source === sources.speakers - ? T.translate("summit_speakers_list.send_emails_title") - : T.translate("summit_submitters_list.send_emails_title")} - - - -
-
- {T.translate("summit_speakers_list.send_email_warning", { - template: currentFlowEvent, - qty: selectedCount - })} -
- {this.state.testRecipient !== "" && ( -
- {T.translate( - "summit_speakers_list.email_test_recipient", - { - email: this.state.testRecipient - } - )} -
- )} -
- -
- -
-
- -
-
- -
- { - this.ingestEmailRef = node; - }} - /> -
- {this.state.source === sources.speakers && ( -
-
- { - this.shouldSendCopy2SubmitterRef = node; - }} - /> - -
-
- )} -
-
- - - - -
-
- )} -
- ); - } -} - -const mapStateToProps = ({ - currentSummitState, - currentSummitSpeakersListState, - currentSummitSubmittersListState, - currentPromocodeSpecificationState -}) => ({ - currentSummit: currentSummitState.currentSummit, - speakersProps: currentSummitSpeakersListState, - submittersProps: currentSummitSubmittersListState, - currentPromocodeSpecification: currentPromocodeSpecificationState -}); - -export default connect(mapStateToProps, { - initSpeakersList, - getSpeakersBySummit, - exportSummitSpeakers, - selectSummitSpeaker, - unselectSummitSpeaker, - selectAllSummitSpeakers, - unselectAllSummitSpeakers, - setCurrentFlowEvent, - sendSpeakerEmails, - initSubmittersList, - getSubmittersBySummit, - exportSummitSubmitters, - selectSummitSubmitter, - unselectSummitSubmitter, - selectAllSummitSubmitters, - unselectAllSummitSubmitters, - setCurrentSubmitterFlowEvent, - sendSubmitterEmails, - validateSpecs, - resetPromoCodeSpecForm -})(SummitSpeakersListPage); diff --git a/src/pages/summit_speakers/summit-speakers-list-page/components/send-email-modal.js b/src/pages/summit_speakers/summit-speakers-list-page/components/send-email-modal.js new file mode 100644 index 000000000..da4d836db --- /dev/null +++ b/src/pages/summit_speakers/summit-speakers-list-page/components/send-email-modal.js @@ -0,0 +1,395 @@ +import React, { useState } from "react"; +import T from "i18n-react"; +import { connect } from "react-redux"; +import Swal from "sweetalert2"; +import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown"; +import Input from "openstack-uicore-foundation/lib/components/inputs/text-input"; +import { Modal } from "react-bootstrap"; +import { validateEmail } from "../../../../utils/methods"; +import { SpeakersSources as sources } from "../../../../utils/constants"; +import SpeakerPromoCodeSpecForm from "../../../../components/forms/speakers-promo-code-spec-form"; +import { + AUTO_GENERATED_SPEAKERS_DISCOUNT_CODE, + AUTO_GENERATED_SPEAKERS_PROMO_CODE, + EXISTING_SPEAKERS_DISCOUNT_CODE, + EXISTING_SPEAKERS_PROMO_CODE +} from "../../../../actions/promocode-actions"; +import { + sendSpeakerEmails, + setCurrentFlowEvent +} from "../../../../actions/speaker-actions"; +import { + sendSubmitterEmails, + setCurrentSubmitterFlowEvent +} from "../../../../actions/submitter-actions"; +import { + resetPromoCodeSpecForm, + validateSpecs +} from "../../../../actions/promocode-specification-actions"; + +const emailFlowSpeakersDDL = [ + { label: "-- SELECT EMAIL EVENT --", value: "" }, + { + label: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ACCEPTED_ALTERNATE", + value: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ACCEPTED_ALTERNATE" + }, + { + label: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ACCEPTED_REJECTED", + value: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ACCEPTED_REJECTED" + }, + { + label: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ALTERNATE_REJECTED", + value: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ALTERNATE_REJECTED" + }, + { + label: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ACCEPTED_ONLY", + value: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ACCEPTED_ONLY" + }, + { + label: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ALTERNATE_ONLY", + value: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_ALTERNATE_ONLY" + }, + { + label: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_REJECTED_ONLY", + value: "SUMMIT_SUBMISSIONS_PRESENTATION_SPEAKER_REJECTED_ONLY" + } +]; +const emailFlowSubmittersDDL = [ + { label: "-- SELECT EMAIL EVENT --", value: "" }, + { + label: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ACCEPTED_ALTERNATE", + value: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ACCEPTED_ALTERNATE" + }, + { + label: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ACCEPTED_REJECTED", + value: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ACCEPTED_REJECTED" + }, + { + label: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ALTERNATE_REJECTED", + value: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ALTERNATE_REJECTED" + }, + { + label: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ACCEPTED_ONLY", + value: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ACCEPTED_ONLY" + }, + { + label: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ALTERNATE_ONLY", + value: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_ALTERNATE_ONLY" + }, + { + label: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_REJECTED_ONLY", + value: "SUMMIT_SUBMISSIONS_PRESENTATION_SUBMITTER_REJECTED_ONLY" + } +]; + +const promoCodeStrategiesDDL = [ + { + label: T.translate("summit_speakers_list.select_promo_code_strategy"), + value: 0 + }, + { + label: T.translate("summit_speakers_list.select_speaker_promo_code"), + value: EXISTING_SPEAKERS_PROMO_CODE + }, + { + label: T.translate("summit_speakers_list.select_speaker_discount_code"), + value: EXISTING_SPEAKERS_DISCOUNT_CODE + }, + { + label: T.translate( + "summit_speakers_list.select_auto_generate_speaker_promo_code" + ), + value: AUTO_GENERATED_SPEAKERS_PROMO_CODE + }, + { + label: T.translate( + "summit_speakers_list.select_auto_generate_speaker_discount_code" + ), + value: AUTO_GENERATED_SPEAKERS_DISCOUNT_CODE + } +]; + +const SendEmailModal = ({ + source, + filters, + speakersProps, + submittersProps, + currentSummit, + currentPromocodeSpecification, + setCurrentFlowEvent, + setCurrentSubmitterFlowEvent, + validateSpecs, + resetPromoCodeSpecForm, + sendSpeakerEmails, + sendSubmitterEmails +}) => { + const [openModal, setOpenModal] = useState(false); + const [promoCodeStrategy, setPromoCodeStrategy] = useState(null); + const [testRecipient, setTestRecipient] = useState(""); + const [modalValues, setModalValues] = useState({ + ingest_email: "", + should_send_copy_2_submitter: false + }); + const isSpeakerMode = source === sources.speakers; + const subjectProps = isSpeakerMode ? speakersProps : submittersProps; + const { currentFlowEvent, selectedCount } = subjectProps; + const emailFlowDDL = isSpeakerMode + ? emailFlowSpeakersDDL + : emailFlowSubmittersDDL; + + const handleOpenModal = (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + + if (!currentFlowEvent) { + Swal.fire( + "Validation error", + T.translate("summit_speakers_list.select_template"), + "warning" + ); + return false; + } + + if (selectedCount === 0) { + const content = + source === sources.speakers + ? T.translate("summit_speakers_list.select_items") + : T.translate("summit_submitters_list.select_items"); + Swal.fire("Validation error", content, "warning"); + return false; + } + + if (testRecipient !== "" && !validateEmail(testRecipient)) { + Swal.fire( + "Validation error", + T.translate("summit_speakers_list.invalid_recipient_email"), + "warning" + ); + return false; + } + + setOpenModal(true); + setModalValues({ ingest_email: "", should_send_copy_2_submitter: false }); + }; + + const handleChangeFlowEvent = (ev) => { + const { value } = ev.target; + if (isSpeakerMode) { + setCurrentFlowEvent(value); + } else { + setCurrentSubmitterFlowEvent(value); + } + }; + + const handleChangePromoCodeStrategy = (ev) => { + const { value } = ev.target; + setPromoCodeStrategy(value); + resetPromoCodeSpecForm(); + }; + + const handleChangeTestRecipient = (ev) => { + const { value } = ev.target; + setTestRecipient(value); + }; + + const handleModalChange = (ev) => { + const { id, value, checked } = ev.target; + setModalValues({ + ...modalValues, + [id]: checked || value + }); + }; + + const onValidationSuccess = () => { + const { + ingest_email: excerptRecipient, + should_send_copy_2_submitter: sendCopy + } = modalValues; + const shouldSendCopy2Submitter = isSpeakerMode && sendCopy; + const { term } = subjectProps; + + setOpenModal(false); + setTestRecipient(""); + setPromoCodeStrategy(null); + setModalValues({ ingest_email: "", should_send_copy_2_submitter: false }); + + // send emails + const sendEmails = isSpeakerMode ? sendSpeakerEmails : sendSubmitterEmails; + + sendEmails( + term, + filters, + testRecipient, + excerptRecipient, + shouldSendCopy2Submitter, + source, + promoCodeStrategy, + currentPromocodeSpecification.entity + ); + }; + + const handleSendEmails = (ev) => { + ev.stopPropagation(); + ev.preventDefault(); + + validateSpecs( + promoCodeStrategy, + currentPromocodeSpecification.entity, + onValidationSuccess + ); + }; + + return ( + <> +
+
+ +
+
+ +
+
+ +
+
+ setOpenModal(false)} + backdrop={false} + > + + + {isSpeakerMode + ? T.translate("summit_speakers_list.send_emails_title") + : T.translate("summit_submitters_list.send_emails_title")} + + + +
+
+ {T.translate("summit_speakers_list.send_email_warning", { + template: currentFlowEvent, + qty: selectedCount + })} +
+ {testRecipient !== "" && ( +
+ {T.translate("summit_speakers_list.email_test_recipient", { + email: testRecipient + })} +
+ )} +
+ +
+ +
+
+ +
+
+ +
+ +
+ {isSpeakerMode && ( +
+
+ + +
+
+ )} +
+
+ + + + +
+ + ); +}; + +const mapStateToProps = ({ + currentSummitState, + currentSummitSpeakersListState, + currentSummitSubmittersListState, + currentPromocodeSpecificationState +}) => ({ + currentSummit: currentSummitState.currentSummit, + speakersProps: currentSummitSpeakersListState, + submittersProps: currentSummitSubmittersListState, + currentPromocodeSpecification: currentPromocodeSpecificationState +}); + +export default connect(mapStateToProps, { + setCurrentFlowEvent, + sendSpeakerEmails, + setCurrentSubmitterFlowEvent, + sendSubmitterEmails, + validateSpecs, + resetPromoCodeSpecForm +})(SendEmailModal); diff --git a/src/pages/summit_speakers/summit-speakers-list-page/index.js b/src/pages/summit_speakers/summit-speakers-list-page/index.js new file mode 100644 index 000000000..22813b9c0 --- /dev/null +++ b/src/pages/summit_speakers/summit-speakers-list-page/index.js @@ -0,0 +1,482 @@ +/** + * Copyright 2017 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 { Pagination } from "react-bootstrap"; +import SelectableTable from "openstack-uicore-foundation/lib/components/table-selectable"; +import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown"; +import FreeTextSearch from "openstack-uicore-foundation/lib/components/free-text-search"; +import GridFilter from "../../../components/GridFilter"; +import useGridFilter from "../../../components/GridFilter/hooks/useGridFilter"; +import { + exportSummitSpeakers, + getSpeakersBySummit, + initSpeakersList, + selectAllSummitSpeakers, + selectSummitSpeaker, + unselectAllSummitSpeakers, + unselectSummitSpeaker +} from "../../../actions/speaker-actions"; +import { + exportSummitSubmitters, + getSubmittersBySummit, + initSubmittersList, + selectAllSummitSubmitters, + selectSummitSubmitter, + unselectAllSummitSubmitters, + unselectSummitSubmitter +} from "../../../actions/submitter-actions"; + +import { SpeakersSources as sources } from "../../../utils/constants"; +import "../../../styles/speakers-list-page.less"; +import SendEmailModal from "./components/send-email-modal"; + +const FILTER_ID = "summit_speakers_list"; + +const selectionStatusOptions = [ + { label: "Accepted", value: "accepted" }, + { label: "Alternate", value: "alternate" }, + { label: "Rejected", value: "rejected" }, + { label: "Only Rejected", value: "only_rejected" }, + { label: "Only Accepted", value: "only_accepted" }, + { label: "Only Alternate", value: "only_alternate" }, + { label: "Accepted/Alternate", value: "accepted_alternate" }, + { label: "Accepted/Rejected", value: "accepted_rejected" }, + { label: "Alternate/Rejected", value: "alternate_rejected" } +]; + +const getCriterias = (summit) => [ + { + key: "presentations_selection_plan_id", + label: "Selection Plan", + operators: [{ value: "==", label: "is" }], + values: { + type: "select", + props: { + options: summit.selection_plans.map((sp) => ({ + label: sp.name, + value: sp.id + })), + placeholder: "Filter by Selection Plan" + } + } + }, + { + key: "presentations_track_id", + label: "Track", + operators: [{ value: "==", label: "is" }], + values: { + type: "select", + props: { + options: summit.tracks.map((t) => ({ label: t.name, value: t.id })), + placeholder: "Filter by Track" + } + } + }, + { + key: "presentations_type_id", + label: "Activity Type", + operators: [{ value: "==", label: "is" }], + values: { + type: "select", + props: { + options: summit.event_types.map((type) => ({ + label: type.name, + value: type.id + })), + placeholder: "Filter by Activity Type" + } + } + }, + { + key: "selection_status", + label: "Selection Status", + operators: [{ value: "==", label: "is" }], + values: { + type: "select", + props: { + options: [...selectionStatusOptions], + placeholder: "Filter by Selection Status" + } + }, + customParser: (f) => { + const filter = []; + if (f.value) { + switch (f.value) { + case "only_rejected": + filter.push("has_rejected_presentations==true"); + filter.push("has_accepted_presentations==false"); + filter.push("has_alternate_presentations==false"); + break; + case "only_accepted": + filter.push("has_rejected_presentations==false"); + filter.push("has_accepted_presentations==true"); + filter.push("has_alternate_presentations==false"); + break; + case "only_alternate": + filter.push("has_rejected_presentations==false"); + filter.push("has_accepted_presentations==false"); + filter.push("has_alternate_presentations==true"); + break; + case "accepted_alternate": + filter.push("has_rejected_presentations==false"); + filter.push("has_accepted_presentations==true"); + filter.push("has_alternate_presentations==true"); + break; + case "accepted_rejected": + filter.push("has_rejected_presentations==true"); + filter.push("has_accepted_presentations==true"); + filter.push("has_alternate_presentations==false"); + break; + case "alternate_rejected": + filter.push("has_rejected_presentations==true"); + filter.push("has_accepted_presentations==false"); + filter.push("has_alternate_presentations==true"); + break; + case "accepted": + filter.push("has_accepted_presentations==true"); + break; + case "rejected": + filter.push("has_rejected_presentations==true"); + break; + case "alternate": + filter.push("has_alternate_presentations==true"); + break; + default: + break; + } + } + return filter; + } + }, + { + key: "presentations_track_group_id", + label: "Track Group", + operators: [{ value: "==", label: "is" }], + values: { + type: "select", + props: { + options: summit.track_groups.map((trackGroup) => ({ + label: trackGroup.name, + value: trackGroup.id + })), + placeholder: "Filter by Track Groups" + } + } + }, + { + key: "has_media_upload_with_type", + label: "Media Upload Type", + operators: [ + { value: ">>", label: "has" }, + { value: "!>>", label: "has not" } + ], + values: { + type: "select", + props: { + options: [{ value: "async", label: "Async" }], + placeholder: "Filter by MediaUploads Type" + } + } + } +]; + +const sourceOptions = [ + { + label: T.translate("summit_speakers_list.speakers"), + value: sources.speakers + }, + { + label: T.translate("summit_submitters_list.submitters"), + value: sources.submitters + }, + { + label: T.translate("summit_submitters_list.submitters_no_speakers"), + value: sources.submitters_no_speakers + } +]; + +const SummitSpeakersListPage = ({ + currentSummit, + history, + speakersProps, + submittersProps, + getSpeakersBySummit, + getSubmittersBySummit, + exportSummitSpeakers, + exportSummitSubmitters, + selectSummitSpeaker, + unselectSummitSpeaker, + selectSummitSubmitter, + unselectSummitSubmitter, + selectAllSummitSpeakers, + selectAllSummitSubmitters, + unselectAllSummitSpeakers, + unselectAllSummitSubmitters +}) => { + const [source, setSource] = useState(sources.speakers); + const isSpeakerMode = source === sources.speakers; + const subjectProps = isSpeakerMode ? speakersProps : submittersProps; + const { parsedFilter } = useGridFilter(FILTER_ID); + + useEffect(() => { + initSubmittersList(); + initSpeakersList(); + + if (currentSummit) { + getBySummit(); + } + }, [currentSummit, source, parsedFilter.join(",")]); + + const getBySummit = (params = {}) => { + const { term, page, perPage, order, orderDir } = subjectProps; + + const mergedParams = { term, page, perPage, order, orderDir, ...params }; + + const getSubjects = isSpeakerMode + ? getSpeakersBySummit + : getSubmittersBySummit; + + const { + term: t, + page: p, + perPage: pp, + order: o, + orderDir: od + } = mergedParams; + + getSubjects(t, p, pp, o, od, parsedFilter, source); + }; + + const handleSourceChange = (ev) => { + const { value } = ev.target; + setSource(value); + }; + + const handleEdit = (itemId) => { + if (isSpeakerMode) { + history.push(`/app/speakers/${itemId}`); + } + }; + + const handlePageChange = (page) => { + getBySummit({ page }); + }; + + const handleSort = (index, key, dir) => { + getBySummit({ order: key, orderDir: dir }); + }; + + const handleSearch = (term) => { + getBySummit({ term }); + }; + + const handleExport = (ev) => { + ev.preventDefault(); + const { term, order, orderDir } = subjectProps; + const exportSubjects = isSpeakerMode + ? exportSummitSpeakers + : exportSummitSubmitters; + + exportSubjects(term, order, orderDir, parsedFilter, source); + }; + + const handleSelected = (itemId, isSelected) => { + const select = isSpeakerMode ? selectSummitSpeaker : selectSummitSubmitter; + const unselect = isSpeakerMode + ? unselectSummitSpeaker + : unselectSummitSubmitter; + + if (isSelected) select(itemId); + else unselect(itemId); + }; + + const handleSelectedAll = (ev) => { + const selectedAll = ev.target.checked; + const selectAll = isSpeakerMode + ? selectAllSummitSpeakers + : selectAllSummitSubmitters; + const unselectAll = isSpeakerMode + ? unselectAllSummitSpeakers + : unselectAllSummitSubmitters; + + if (selectedAll) selectAll(); + else unselectAll(); + }; + + const { + items, + lastPage, + currentPage, + term, + order, + orderDir, + totalItems, + selectedCount, + selectedAll + } = subjectProps; + + const columns = [ + { + columnKey: "full_name", + value: T.translate("general.name"), + sortable: true + }, + { + columnKey: "email", + value: T.translate("general.email"), + sortable: true + }, + { + columnKey: "accepted_presentations_count", + value: T.translate("summit_speakers_list.accepted") + }, + { + columnKey: "alternate_presentations_count", + value: T.translate("summit_speakers_list.alternate") + }, + { + columnKey: "rejected_presentations_count", + value: T.translate("summit_speakers_list.rejected") + } + ]; + + const tableOptions = { + sortCol: order, + sortDir: orderDir, + actions: { + edit: { + onClick: handleEdit, + onSelected: handleSelected, + onSelectedAll: handleSelectedAll + } + }, + selectedAll + }; + + if (!currentSummit.id) return
; + + return ( +
+

+ {" "} + {isSpeakerMode + ? T.translate("summit_speakers_list.summit_speakers_list") + : T.translate("summit_submitters_list.summit_submitters_list")}{" "} + ({totalItems}) +

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + {items.length === 0 && ( +
+ {isSpeakerMode + ? T.translate("summit_speakers_list.no_speakers") + : T.translate("summit_submitters_list.no_submitters")} +
+ )} + + {items.length > 0 && ( +
+ + + {T.translate("summit_speakers_list.items_qty", { + qty: selectedCount + })} + + + + +
+ )} +
+ ); +}; + +const mapStateToProps = ({ + currentSummitState, + currentSummitSpeakersListState, + currentSummitSubmittersListState +}) => ({ + currentSummit: currentSummitState.currentSummit, + speakersProps: currentSummitSpeakersListState, + submittersProps: currentSummitSubmittersListState +}); + +export default connect(mapStateToProps, { + initSpeakersList, + getSpeakersBySummit, + exportSummitSpeakers, + selectSummitSpeaker, + unselectSummitSpeaker, + selectAllSummitSpeakers, + unselectAllSummitSpeakers, + initSubmittersList, + getSubmittersBySummit, + exportSummitSubmitters, + selectSummitSubmitter, + unselectSummitSubmitter, + selectAllSummitSubmitters, + unselectAllSummitSubmitters +})(SummitSpeakersListPage); diff --git a/src/store.js b/src/store.js index 2fbc1a166..f791c4473 100644 --- a/src/store.js +++ b/src/store.js @@ -16,6 +16,7 @@ import { loggedUserReducer } from "openstack-uicore-foundation/lib/security/redu import thunk from "redux-thunk"; import { persistStore, persistCombineReducers } from "redux-persist"; import storage from "redux-persist/es/storage"; +import allFiltersReducer from "./components/GridFilter/reducers/all-filters-reducer"; import baseReducer from "./reducers/base-reducer"; import currentSummitReducer from "./reducers/summits/current-summit-reducer"; import directoryReducer from "./reducers/summits/directory-reducer"; @@ -184,6 +185,7 @@ const config = { const reducers = persistCombineReducers(config, { loggedUserState: loggedUserReducer, + allGridFiltersState: allFiltersReducer, baseState: baseReducer, directoryState: directoryReducer, currentSummitState: currentSummitReducer, diff --git a/yarn.lock b/yarn.lock index 210f5d00f..201f65e95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10013,7 +10013,7 @@ react-input-autosize@^2.2.1: dependencies: prop-types "^15.5.8" -react-is@^16.13.1, react-is@^16.3.2, react-is@^16.6.0, react-is@^16.7.0: +react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -10033,7 +10033,7 @@ react-is@^19.0.0, react-is@^19.2.3: resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.5.tgz#7e7b54143e9313fed787b23fd4295d5a23872ad9" integrity sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ== -react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: +react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== @@ -10054,20 +10054,7 @@ react-overlays@^0.7.4: prop-types-extra "^1.0.1" warning "^3.0.0" -react-redux@^5.0.7: - version "5.1.2" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57" - integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q== - dependencies: - "@babel/runtime" "^7.1.2" - hoist-non-react-statics "^3.3.0" - invariant "^2.2.4" - loose-envify "^1.1.0" - prop-types "^15.6.1" - react-is "^16.6.0" - react-lifecycles-compat "^3.0.0" - -react-redux@^7.2.0: +react-redux@^7.1.0, react-redux@^7.2.0: version "7.2.9" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==