From d83b2c36d29cf7d590651d34ce229f55750bd383 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 7 May 2026 17:56:58 -0300 Subject: [PATCH 1/4] chore: building ui - WIP --- .../GridFilter/components/Dropdown.jsx | 78 ++++++++++++ .../GridFilter/components/Filter.jsx | 106 +++++++++++++++++ .../GridFilter/components/ToggleButtons.jsx | 46 +++++++ .../components/ValueInput/index.jsx | 47 ++++++++ src/components/GridFilter/index.jsx | 112 ++++++++++++++++++ 5 files changed, 389 insertions(+) create mode 100644 src/components/GridFilter/components/Dropdown.jsx create mode 100644 src/components/GridFilter/components/Filter.jsx create mode 100644 src/components/GridFilter/components/ToggleButtons.jsx create mode 100644 src/components/GridFilter/components/ValueInput/index.jsx create mode 100644 src/components/GridFilter/index.jsx diff --git a/src/components/GridFilter/components/Dropdown.jsx b/src/components/GridFilter/components/Dropdown.jsx new file mode 100644 index 000000000..f124ae7e3 --- /dev/null +++ b/src/components/GridFilter/components/Dropdown.jsx @@ -0,0 +1,78 @@ +/** + * 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; \ No newline at end of file diff --git a/src/components/GridFilter/components/Filter.jsx b/src/components/GridFilter/components/Filter.jsx new file mode 100644 index 000000000..2bfb604cd --- /dev/null +++ b/src/components/GridFilter/components/Filter.jsx @@ -0,0 +1,106 @@ +/** + * 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, Grid2 } from "react-bootstrap"; +import DeleteIcon from "@mui/icons-material/Delete"; +import AddIcon from "@mui/icons-material/Add"; +import PropTypes from "prop-types"; +import Box from "@mui/material/Box"; +import Dropdown from "./Dropdown"; +import ValueInput from "./ValueInput"; + +const Filter = ({ id, value, criteriaSettings, operatorSettings, valueSettings, onChange, onAdd, onDelete }) => { + const handleChange = (prop, val) => { + onChange({ ...value, [prop]: val }); + }; + + return ( + + + + handleChange("criteria", val)} + /> + handleChange("operator", val)} + /> + handleChange("value", val)} + /> + + + + + + + + ); +}; + +Filter.propTypes = { + id: PropTypes.string, + value: PropTypes.shape({ + criteria: PropTypes.string, + operator: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool, PropTypes.array]) + }), + criteriaSettings: PropTypes.shape({ + options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + label: PropTypes.string + })).isRequired, + placeholder: PropTypes.string + }).isRequired, + operatorSettings: PropTypes.shape({ + options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + label: PropTypes.string + })).isRequired, + placeholder: PropTypes.string + }).isRequired, + valueSettings: PropTypes.shape({ + type: PropTypes.string, // class name of the component to render the value + props: PropTypes.object, // props to pass to the component + }).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/ToggleButtons.jsx b/src/components/GridFilter/components/ToggleButtons.jsx new file mode 100644 index 000000000..d7eddfb13 --- /dev/null +++ b/src/components/GridFilter/components/ToggleButtons.jsx @@ -0,0 +1,46 @@ +/** + * 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 "react-bootstrap"; +import PropTypes from "prop-types"; + +const ToggleButtons = ({ options, value, onChange }) => { + return ( + + {options.map((option) => ( + + {option} + + ))} + + ); +}; + +ToggleButtons.propTypes = { + options: PropTypes.arrayOf(PropTypes.string).isRequired, + value: PropTypes.string, + onChange: PropTypes.func.isRequired, +} + +ToggleButtons.defaultProps = { + value: null, +} + +export default ToggleButtons; \ No newline at end of file diff --git a/src/components/GridFilter/components/ValueInput/index.jsx b/src/components/GridFilter/components/ValueInput/index.jsx new file mode 100644 index 000000000..227b9b0d9 --- /dev/null +++ b/src/components/GridFilter/components/ValueInput/index.jsx @@ -0,0 +1,47 @@ +/** + * 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 = INPUT_TYPE_MAP[type]; + // 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; \ No newline at end of file diff --git a/src/components/GridFilter/index.jsx b/src/components/GridFilter/index.jsx new file mode 100644 index 000000000..dbd6a6420 --- /dev/null +++ b/src/components/GridFilter/index.jsx @@ -0,0 +1,112 @@ +/** + * 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, { useState } from "react"; +import T from "i18n-react/dist/i18n-react"; +import { Button, Dialog, DialogActions, DialogContent, Divider, IconButton, Typography } from "@mui/material"; +import FilterListIcon from "@mui/icons-material/FilterList"; +import ToggleButtons from "./components/ToggleButtons"; +import Box from "@mui/material/Box"; +import Filter from "./components/Filter"; + +// sample settings +/* +settings = { + criteria: { + options: [ + { value: "name", label: "Name" }, + { value: "email", label: "Email" }, + ], + placeholder: "Select a criteria", + }, + operator: { + options: [ + { value: "eq", label: "Equals" }, + { value: "ne", label: "Not Equals" }, + { value: "gt", label: "Greater Than" }, + { value: "lt", label: "Less Than" }, + ], + placeholder: "Select an operator", + }, + value: { + type: "ValueInput", // class name of the component to render the value + props: { + type: "text", // props to pass to the component + }, + } +} + + + */ + +const GridFilter = ({values, settings}) => { + const [openModal, setOpenModal] = useState(false); + + + const handleChange = (val) => {} + + const handleClear = () => {} + + const handleSubmit = () => {} + + return ( + <> + setOpenModal(true)} + sx={{ mr: 1 }} + > + + + setOpenModal(false)} + maxWidth="md" + fullWidth + > + + {T.translate("grid_filter.filter_by")} + handleChange(val)} /> + {T.translate("grid_filter.following")} + + + {values.map(({criteria, value}, index) => ( + {}} + onAdd={() => {}} + onDelete={() => {}} + /> + ))} + + + + + + + + + + + ); +}; From c33d2c9d270e0a9f19d3fa70b49f19b55b0fd193 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Fri, 8 May 2026 09:16:30 -0300 Subject: [PATCH 2/4] chore: define contract --- .../GridFilter/components/Filter.jsx | 46 +++++-- src/components/GridFilter/index.jsx | 121 ++++++++++++------ 2 files changed, 119 insertions(+), 48 deletions(-) diff --git a/src/components/GridFilter/components/Filter.jsx b/src/components/GridFilter/components/Filter.jsx index 2bfb604cd..66f5f6cb9 100644 --- a/src/components/GridFilter/components/Filter.jsx +++ b/src/components/GridFilter/components/Filter.jsx @@ -20,7 +20,19 @@ import Box from "@mui/material/Box"; import Dropdown from "./Dropdown"; import ValueInput from "./ValueInput"; -const Filter = ({ id, value, criteriaSettings, operatorSettings, valueSettings, onChange, onAdd, onDelete }) => { +const Filter = ({ + id, + value, + criteria, + criteriaOptions, + onChange, + onAdd, + onDelete +}) => { + const criteriaSettings = { options: criteriaOptions }; + const operatorSettings = criteria.operators; + const valueSettings = criteria.values; + const handleChange = (prop, val) => { onChange({ ...value, [prop]: val }); }; @@ -32,6 +44,7 @@ const Filter = ({ id, value, criteriaSettings, operatorSettings, valueSettings, handleChange("criteria", val)} @@ -39,6 +52,7 @@ const Filter = ({ id, value, criteriaSettings, operatorSettings, valueSettings, handleChange("operator", val)} @@ -47,6 +61,7 @@ const Filter = ({ id, value, criteriaSettings, operatorSettings, valueSettings, id={`${id}-value`} value={value.value} type={valueSettings.type} + placeholder={T.translate("grid_filter.select_values")} // eslint-disable-next-line react/jsx-props-no-spreading {...valueSettings.props} onChange={(val) => handleChange("value", val)} @@ -74,25 +89,34 @@ Filter.propTypes = { value: PropTypes.shape({ criteria: PropTypes.string, operator: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool, PropTypes.array]) + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.bool, + PropTypes.array + ]) }), criteriaSettings: PropTypes.shape({ - options: PropTypes.arrayOf(PropTypes.shape({ - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - label: PropTypes.string - })).isRequired, + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + label: PropTypes.string + }) + ).isRequired, placeholder: PropTypes.string }).isRequired, operatorSettings: PropTypes.shape({ - options: PropTypes.arrayOf(PropTypes.shape({ - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - label: PropTypes.string - })).isRequired, + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + label: PropTypes.string + }) + ).isRequired, placeholder: PropTypes.string }).isRequired, valueSettings: PropTypes.shape({ type: PropTypes.string, // class name of the component to render the value - props: PropTypes.object, // props to pass to the component + props: PropTypes.object // props to pass to the component }).isRequired, onChange: PropTypes.func.isRequired, onAdd: PropTypes.func.isRequired, diff --git a/src/components/GridFilter/index.jsx b/src/components/GridFilter/index.jsx index dbd6a6420..af2a00f0d 100644 --- a/src/components/GridFilter/index.jsx +++ b/src/components/GridFilter/index.jsx @@ -13,51 +13,96 @@ import React, { useState } from "react"; import T from "i18n-react/dist/i18n-react"; -import { Button, Dialog, DialogActions, DialogContent, Divider, IconButton, Typography } from "@mui/material"; +import { Button, Box, Dialog, DialogActions, DialogContent, Divider, IconButton, Typography } from "@mui/material"; import FilterListIcon from "@mui/icons-material/FilterList"; import ToggleButtons from "./components/ToggleButtons"; -import Box from "@mui/material/Box"; import Filter from "./components/Filter"; -// sample settings + +// sample props /* -settings = { - criteria: { - options: [ - { value: "name", label: "Name" }, - { value: "email", label: "Email" }, - ], - placeholder: "Select a criteria", - }, - operator: { - options: [ - { value: "eq", label: "Equals" }, - { value: "ne", label: "Not Equals" }, - { value: "gt", label: "Greater Than" }, - { value: "lt", label: "Less Than" }, - ], - placeholder: "Select an operator", - }, - value: { - type: "ValueInput", // class name of the component to render the value - props: { - type: "text", // props to pass to the component +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: "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 = ({values, settings}) => { +const GridFilter = ({values, criterias}) => { const [openModal, setOpenModal] = useState(false); + const [filters, setFilters] = useState(values); + const criteriaOptions = criterias.map(c => ({label: c.label, value: c.key})) + + const handleChange = (filter) => { + setFilters(prevFilters => ({...prevFilters, filter})) + console.log("change filter", filter); + } - const handleChange = (val) => {} + const handleAdd = () => { + const emptyFilter = {}; + setFilters(prevFilters => ({...prevFilters, emptyFilter})) + console.log("add filter"); + } - const handleClear = () => {} + const handleRemove = (filter) => { + setFilters(prevFilters => prevFilters.filter(f => f !== filter.criteria)) + console.log("remove filter", filter); + } - const handleSubmit = () => {} + const handleClear = () => { + console.log("clear filters"); + } + + const handleSubmit = () => { + console.log("save filters", filters); + } return ( <> @@ -80,18 +125,20 @@ const GridFilter = ({values, settings}) => { {T.translate("grid_filter.following")} - {values.map(({criteria, value}, index) => ( + {values.map((value, index) => { + const criteria = criterias.find(c => c.key === value.criteria); + + return ( {}} - onAdd={() => {}} - onDelete={() => {}} + onChange={handleChange} + onAdd={handleAdd} + onDelete={handleRemove} /> - ))} + )})} From 68e0d26a3eda94520aaff5abca41708ed5de0f8a Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Mon, 11 May 2026 19:03:24 -0300 Subject: [PATCH 3/4] chore: refactor speaker list page --- .../GridFilter/components/Filter.jsx | 127 +- .../GridFilter/components/ToggleButtons.jsx | 6 +- .../components/ValueInput/index.jsx | 1 + src/components/GridFilter/index.jsx | 143 +- src/i18n/en.json | 10 + .../summit-speakers-list-page.js | 1225 ----------------- .../components/send-email-modal.js | 395 ++++++ .../summit-speakers-list-page/index.js | 421 ++++++ 8 files changed, 1020 insertions(+), 1308 deletions(-) delete mode 100644 src/pages/summit_speakers/summit-speakers-list-page.js create mode 100644 src/pages/summit_speakers/summit-speakers-list-page/components/send-email-modal.js create mode 100644 src/pages/summit_speakers/summit-speakers-list-page/index.js diff --git a/src/components/GridFilter/components/Filter.jsx b/src/components/GridFilter/components/Filter.jsx index 66f5f6cb9..50e7efc33 100644 --- a/src/components/GridFilter/components/Filter.jsx +++ b/src/components/GridFilter/components/Filter.jsx @@ -11,81 +11,114 @@ * limitations under the License. * */ -import React from "react"; -import { Button, Grid2 } from "react-bootstrap"; +import React, { useEffect, useState } from "react"; +import T from "i18n-react/dist/i18n-react"; +import { Grid2, Button, Box } from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; import AddIcon from "@mui/icons-material/Add"; import PropTypes from "prop-types"; -import Box from "@mui/material/Box"; import Dropdown from "./Dropdown"; import ValueInput from "./ValueInput"; const Filter = ({ id, value, - criteria, - criteriaOptions, + criterias, onChange, onAdd, onDelete }) => { - const criteriaSettings = { options: criteriaOptions }; - const operatorSettings = criteria.operators; - const valueSettings = criteria.values; + const [selectedCriteria, setSelectedCriteria] = useState(null); + const [selectedOperator, setSelectedOperator] = useState(null); + const [selectedValue, setSelectedValue] = useState(null); + const criteriaOptions = criterias.map(({ key, label }) => ({ value: key, label })); + const criteriaObj = criterias.find(({ key }) => key === selectedCriteria); + const operatorOptions = criteriaObj?.operators || []; + const valueSettings = criteriaObj?.values || {}; + + useEffect(() => { + if(value){ + setSelectedCriteria(value.criteria); + setSelectedOperator(value.operator); + setSelectedValue(value.value); + } + }, [value]) const handleChange = (prop, val) => { onChange({ ...value, [prop]: val }); }; + + // TODO: no es mejor hacer el change en el state del padre ??? pq guardo el state aca ??? + + const handleChangeCriteria = (ev) => { + const val = ev.target.value; + setSelectedCriteria(val); + handleChange("criteria", val); + } + + const handleChangeOperator = (ev) => { + const val = ev.target.value; + setSelectedOperator(val); + handleChange("operator", val); + } + + const handleChangeValue = (ev) => { + const val = ev.target.value; + setSelectedValue(val); + handleChange("value", val); + } + return ( handleChange("criteria", val)} + options={criteriaOptions} + onChange={handleChangeCriteria} /> handleChange("operator", val)} + options={operatorOptions} + onChange={handleChangeOperator} /> handleChange("value", val)} + onChange={handleChangeValue} /> - - + {value ? ( + + ) : ( + + )} ); }; Filter.propTypes = { - id: PropTypes.string, + id: PropTypes.string.isRequired, value: PropTypes.shape({ criteria: PropTypes.string, operator: PropTypes.string, @@ -96,28 +129,22 @@ Filter.propTypes = { PropTypes.array ]) }), - criteriaSettings: PropTypes.shape({ - options: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - label: PropTypes.string - }) - ).isRequired, - placeholder: PropTypes.string - }).isRequired, - operatorSettings: PropTypes.shape({ - options: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - label: PropTypes.string + 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, - placeholder: PropTypes.string - }).isRequired, - valueSettings: PropTypes.shape({ - type: PropTypes.string, // class name of the component to render the value - props: PropTypes.object // props to pass to the component - }).isRequired, + }) + ).isRequired, onChange: PropTypes.func.isRequired, onAdd: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired diff --git a/src/components/GridFilter/components/ToggleButtons.jsx b/src/components/GridFilter/components/ToggleButtons.jsx index d7eddfb13..551bf56d7 100644 --- a/src/components/GridFilter/components/ToggleButtons.jsx +++ b/src/components/GridFilter/components/ToggleButtons.jsx @@ -15,9 +15,9 @@ import React from "react"; import { ToggleButtonGroup, ToggleButton } from "react-bootstrap"; import PropTypes from "prop-types"; -const ToggleButtons = ({ options, value, onChange }) => { - return ( +const ToggleButtons = ({ name, options, value, onChange }) => ( { ))} ); -}; ToggleButtons.propTypes = { + name: PropTypes.string.isRequired, options: PropTypes.arrayOf(PropTypes.string).isRequired, value: PropTypes.string, onChange: PropTypes.func.isRequired, diff --git a/src/components/GridFilter/components/ValueInput/index.jsx b/src/components/GridFilter/components/ValueInput/index.jsx index 227b9b0d9..853750168 100644 --- a/src/components/GridFilter/components/ValueInput/index.jsx +++ b/src/components/GridFilter/components/ValueInput/index.jsx @@ -20,6 +20,7 @@ const INPUT_TYPE_MAP = { text: TextField, select: Dropdown }; const ValueInput = ({type, ...rest}) => { const Component = INPUT_TYPE_MAP[type]; + console.log(type, typeof Component); // eslint-disable-next-line react/jsx-props-no-spreading return Component ? : null; } diff --git a/src/components/GridFilter/index.jsx b/src/components/GridFilter/index.jsx index af2a00f0d..a3eef55cb 100644 --- a/src/components/GridFilter/index.jsx +++ b/src/components/GridFilter/index.jsx @@ -12,12 +12,36 @@ * */ import React, { useState } from "react"; +import PropTypes from "prop-types"; import T from "i18n-react/dist/i18n-react"; -import { Button, Box, Dialog, DialogActions, DialogContent, Divider, IconButton, Typography } from "@mui/material"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + Divider, + IconButton, + Typography +} from "@mui/material"; import FilterListIcon from "@mui/icons-material/FilterList"; import ToggleButtons from "./components/ToggleButtons"; import Filter from "./components/Filter"; +const OPERATORS = [ + { value: "==", label: "is" }, + { value: "=@", label: "like" }, + { value: "@@", label: "like start" }, + { value: "<>", label: "is not" }, + { value: ">>", label: "has" }, + { value: "!>>", label: "has not" }, + { value: "<", label: "less than" }, + { value: "<=", label: "less than or equal to" }, + { value: ">", label: "greater than" }, + { value: ">=", label: "greater than or equal to" }, + { value: "[]", label: "between" }, + { value: "()", label: "between strict" } +]; // sample props /* @@ -74,44 +98,47 @@ value = [ */ -const GridFilter = ({values, criterias}) => { +const GridFilter = ({ values, criterias, onApply }) => { const [openModal, setOpenModal] = useState(false); const [filters, setFilters] = useState(values); - const criteriaOptions = criterias.map(c => ({label: c.label, value: c.key})) - + const criteriaOptions = criterias.map((c) => ({ + label: c.label, + value: c.key + })); const handleChange = (filter) => { - setFilters(prevFilters => ({...prevFilters, filter})) + setFilters((prevFilters) => ({ ...prevFilters, filter })); console.log("change filter", filter); - } + }; const handleAdd = () => { - const emptyFilter = {}; - setFilters(prevFilters => ({...prevFilters, emptyFilter})) console.log("add filter"); - } + }; const handleRemove = (filter) => { - setFilters(prevFilters => prevFilters.filter(f => f !== filter.criteria)) + setFilters((prevFilters) => + prevFilters.filter((f) => f !== filter.criteria) + ); console.log("remove filter", filter); - } + }; const handleClear = () => { console.log("clear filters"); - } + }; const handleSubmit = () => { console.log("save filters", filters); - } + onApply(filters); + }; return ( <> setOpenModal(true)} - sx={{ mr: 1 }} + sx={{ mr: 1, top: "-6px", position: "relative" }} > - + { fullWidth > - {T.translate("grid_filter.filter_by")} - handleChange(val)} /> - {T.translate("grid_filter.following")} + + + {T.translate("grid_filter.filter_by")} + + handleChange(val)} + name="and-or-any" + /> + + {T.translate("grid_filter.following")} + + {values.map((value, index) => { - const criteria = criterias.find(c => c.key === value.criteria); + const criteria = criterias.find((c) => c.key === value.criteria); return ( - - )})} + + ); + })} + @@ -157,3 +202,41 @@ const GridFilter = ({values, criterias}) => { ); }; + +GridFilter.propTypes = { + values: PropTypes.arrayOf( + PropTypes.shape({ + criteria: PropTypes.string.isRequired, + operator: PropTypes.string.isRequired, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + ) + ]).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 +}; + +GridFilter.defaultProps = { + values: [] +}; + +export default GridFilter; diff --git a/src/i18n/en.json b/src/i18n/en.json index 9c4f9fd05..2d792dc1a 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -4117,5 +4117,15 @@ "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_value": "Select value", + "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..903dca1ce --- /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, + filterValues, + 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, + filterValues, + 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..c96f74657 --- /dev/null +++ b/src/pages/summit_speakers/summit-speakers-list-page/index.js @@ -0,0 +1,421 @@ +/** + * 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 { + 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 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: "selection_plan", + 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: "track", + 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: "activity_type", + 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" + } + } + }, + { + key: "track_group", + 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: "media_upload_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 = ({ + filterValues, + 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; + + useEffect(() => { + initSubmittersList(); + initSpeakersList(); + + if (currentSummit) { + getBySummit(); + } + }, [currentSummit, source]); + + 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, filterValues, 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, filterValues, 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); From ebe895deb5cec826e7193f793937d03bdbb6bb4c Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Tue, 12 May 2026 18:48:29 -0300 Subject: [PATCH 4/4] chore: continue work on filter - WIP --- package.json | 2 +- src/actions/speaker-actions.js | 5 +- .../GridFilter/actions/filter-actions.js | 9 + .../GridFilter/components/Dropdown.jsx | 91 +++---- .../GridFilter/components/Filter.jsx | 82 +++---- .../GridFilter/components/FilterButton.jsx | 59 +++++ .../GridFilter/components/RoundButton.jsx | 44 ++++ .../GridFilter/components/ToggleButtons.jsx | 63 +++-- .../components/ValueInput/index.jsx | 32 ++- .../GridFilter/hooks/useGridFilter.jsx | 31 +++ src/components/GridFilter/index.jsx | 224 +++++++++++------- src/components/GridFilter/readme.md | 6 + .../reducers/all-filters-reducer.js | 43 ++++ .../GridFilter/reducers/filter-reducer.js | 33 +++ src/components/GridFilter/utils.js | 16 ++ src/i18n/en.json | 3 +- .../components/send-email-modal.js | 4 +- .../summit-speakers-list-page/index.js | 103 ++++++-- src/store.js | 2 + yarn.lock | 19 +- 20 files changed, 615 insertions(+), 256 deletions(-) create mode 100644 src/components/GridFilter/actions/filter-actions.js create mode 100644 src/components/GridFilter/components/FilterButton.jsx create mode 100644 src/components/GridFilter/components/RoundButton.jsx create mode 100644 src/components/GridFilter/hooks/useGridFilter.jsx create mode 100644 src/components/GridFilter/readme.md create mode 100644 src/components/GridFilter/reducers/all-filters-reducer.js create mode 100644 src/components/GridFilter/reducers/filter-reducer.js create mode 100644 src/components/GridFilter/utils.js 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 index f124ae7e3..fbd66e184 100644 --- a/src/components/GridFilter/components/Dropdown.jsx +++ b/src/components/GridFilter/components/Dropdown.jsx @@ -13,66 +13,71 @@ import React from "react"; import T from "i18n-react/dist/i18n-react"; -import { - Select, - FormControl, - MenuItem, - InputLabel -} from "@mui/material"; +import { Select, FormControl, MenuItem, InputLabel } from "@mui/material"; import PropTypes from "prop-types"; - -const Dropdown = ({id, value, options, placeholder, label, onChange, ...rest}) => { +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, + 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, -} + onChange: PropTypes.func.isRequired +}; Dropdown.defaultProps = { value: null, label: "", - placeholder: "", -} + placeholder: "" +}; -export default Dropdown; \ No newline at end of file +export default Dropdown; diff --git a/src/components/GridFilter/components/Filter.jsx b/src/components/GridFilter/components/Filter.jsx index 50e7efc33..3674e0774 100644 --- a/src/components/GridFilter/components/Filter.jsx +++ b/src/components/GridFilter/components/Filter.jsx @@ -11,87 +11,69 @@ * limitations under the License. * */ -import React, { useEffect, useState } from "react"; +import React from "react"; import T from "i18n-react/dist/i18n-react"; -import { Grid2, Button, Box } from "@mui/material"; +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 [selectedCriteria, setSelectedCriteria] = useState(null); - const [selectedOperator, setSelectedOperator] = useState(null); - const [selectedValue, setSelectedValue] = useState(null); - const criteriaOptions = criterias.map(({ key, label }) => ({ value: key, label })); - const criteriaObj = criterias.find(({ key }) => key === selectedCriteria); +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 || {}; - useEffect(() => { - if(value){ - setSelectedCriteria(value.criteria); - setSelectedOperator(value.operator); - setSelectedValue(value.value); - } - }, [value]) - const handleChange = (prop, val) => { onChange({ ...value, [prop]: val }); }; - - // TODO: no es mejor hacer el change en el state del padre ??? pq guardo el state aca ??? - const handleChangeCriteria = (ev) => { const val = ev.target.value; - setSelectedCriteria(val); handleChange("criteria", val); - } + }; const handleChangeOperator = (ev) => { const val = ev.target.value; - setSelectedOperator(val); handleChange("operator", val); - } + }; const handleChangeValue = (ev) => { const val = ev.target.value; - setSelectedValue(val); handleChange("value", val); - } + }; return ( - + - + - {value ? ( - + + ) : ( - + onAdd()} + disabled={!value?.criteria || !value?.operator || !value?.value} + sx={{ ml: "4px" }} + > + + )} 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 index 551bf56d7..542a3a580 100644 --- a/src/components/GridFilter/components/ToggleButtons.jsx +++ b/src/components/GridFilter/components/ToggleButtons.jsx @@ -12,35 +12,54 @@ * */ import React from "react"; -import { ToggleButtonGroup, ToggleButton } from "react-bootstrap"; +import { ToggleButtonGroup, ToggleButton } from "@mui/material"; import PropTypes from "prop-types"; -const ToggleButtons = ({ name, options, value, onChange }) => ( - - {options.map((option) => ( - - {option} - - ))} - - ); +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 = { - name: PropTypes.string.isRequired, options: PropTypes.arrayOf(PropTypes.string).isRequired, value: PropTypes.string, - onChange: PropTypes.func.isRequired, -} + color: PropTypes.string, + onChange: PropTypes.func.isRequired +}; ToggleButtons.defaultProps = { value: null, -} + color: "primary" +}; -export default ToggleButtons; \ No newline at end of file +export default ToggleButtons; diff --git a/src/components/GridFilter/components/ValueInput/index.jsx b/src/components/GridFilter/components/ValueInput/index.jsx index 853750168..5c457e2fb 100644 --- a/src/components/GridFilter/components/ValueInput/index.jsx +++ b/src/components/GridFilter/components/ValueInput/index.jsx @@ -18,31 +18,37 @@ import Dropdown from "../Dropdown"; const INPUT_TYPE_MAP = { text: TextField, select: Dropdown }; -const ValueInput = ({type, ...rest}) => { - const Component = INPUT_TYPE_MAP[type]; - console.log(type, typeof Component); +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, - })), + 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, -} + onChange: PropTypes.func.isRequired +}; ValueInput.defaultProps = { value: null, label: "", placeholder: "", options: null -} +}; -export default ValueInput; \ No newline at end of file +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 index a3eef55cb..21f7ca57d 100644 --- a/src/components/GridFilter/index.jsx +++ b/src/components/GridFilter/index.jsx @@ -11,8 +11,9 @@ * limitations under the License. * */ -import React, { useState } from "react"; +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, @@ -21,27 +22,14 @@ import { DialogActions, DialogContent, Divider, - IconButton, Typography } from "@mui/material"; -import FilterListIcon from "@mui/icons-material/FilterList"; import ToggleButtons from "./components/ToggleButtons"; import Filter from "./components/Filter"; - -const OPERATORS = [ - { value: "==", label: "is" }, - { value: "=@", label: "like" }, - { value: "@@", label: "like start" }, - { value: "<>", label: "is not" }, - { value: ">>", label: "has" }, - { value: "!>>", label: "has not" }, - { value: "<", label: "less than" }, - { value: "<=", label: "less than or equal to" }, - { value: ">", label: "greater than" }, - { value: ">=", label: "greater than or equal to" }, - { value: "[]", label: "between" }, - { value: "()", label: "between strict" } -]; +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 /* @@ -65,6 +53,41 @@ criterias = [ }, }, }, + { + 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", @@ -94,101 +117,138 @@ value = [ value: "openstack" } ] - - */ -const GridFilter = ({ values, criterias, onApply }) => { +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(values); - const criteriaOptions = criterias.map((c) => ({ - label: c.label, - value: c.key - })); + 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, filter })); - console.log("change filter", filter); + setFilters((prevFilters) => + prevFilters.map((f) => (f.id === filter.id ? filter : f)) + ); }; const handleAdd = () => { - console.log("add filter"); + 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 !== filter.criteria) - ); - console.log("remove filter", filter); + setFilters((prevFilters) => prevFilters.filter((f) => f.id !== filter.id)); }; const handleClear = () => { - console.log("clear filters"); + setFilters([EMPTY_FILTER]); }; const handleSubmit = () => { - console.log("save filters", filters); - onApply(filters); + // 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)} - sx={{ mr: 1, top: "-6px", position: "relative" }} - > - - + onDelete={handleRemoveAndApply} + /> setOpenModal(false)} maxWidth="md" fullWidth > - + - + {T.translate("grid_filter.filter_by")} handleChange(val)} + options={Object.values(JOIN_OPERATORS)} + value={andOrAny} + onChange={setAndOrAny} name="and-or-any" /> - + {T.translate("grid_filter.following")} - + - {values.map((value, index) => { - const criteria = criterias.find((c) => c.key === value.criteria); - - return ( - - ); - })} - + {filters.map((filter) => ( + + ))} -
-
+
-
- +
+
- + {items.length === 0 && (
@@ -396,7 +457,7 @@ const SummitSpeakersListPage = ({ const mapStateToProps = ({ currentSummitState, currentSummitSpeakersListState, - currentSummitSubmittersListState, + currentSummitSubmittersListState }) => ({ currentSummit: currentSummitState.currentSummit, speakersProps: currentSummitSpeakersListState, 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==