diff --git a/frontend/src/components/buttons/StepButtons.js b/frontend/src/components/buttons/StepButtons.js
new file mode 100644
index 000000000..63cfd20eb
--- /dev/null
+++ b/frontend/src/components/buttons/StepButtons.js
@@ -0,0 +1,31 @@
+import React from 'react';
+
+import { makeStyles } from '@material-ui/core/styles';
+import { Button, Box } from '@material-ui/core';
+
+const useButtonStyles = makeStyles(theme => ({
+ button: {
+ marginLeft: theme.spacing(2)
+ }
+}));
+
+const StepButtons = ({ numSteps, activeStep, onBack, onNext, onFinish }) => {
+ const classes = useButtonStyles();
+ return (
+
+
+ Back
+
+
+ {activeStep === numSteps - 1 ? 'Finish' : 'Next'}
+
+
+ );
+};
+
+export default StepButtons;
diff --git a/frontend/src/components/filters/FilterForm.js b/frontend/src/components/filters/FilterForm.js
new file mode 100644
index 000000000..220298215
--- /dev/null
+++ b/frontend/src/components/filters/FilterForm.js
@@ -0,0 +1,153 @@
+import React, { useState, useMemo, useCallback, useEffect } from 'react';
+
+import { connect } from 'react-redux';
+import { RegistrationFields, FilterTypes } from '@hackjunction/shared';
+import { makeStyles } from '@material-ui/core/styles';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import {
+ Grid,
+ Button,
+ ExpansionPanel,
+ ExpansionPanelSummary,
+ ExpansionPanelDetails,
+ ExpansionPanelActions,
+ Typography
+} from '@material-ui/core';
+
+import Select from 'components/inputs/Select';
+import FilterValueInput from './FilterValueInput';
+import * as OrganiserSelectors from 'redux/organiser/selectors';
+
+const useStyles = makeStyles(theme => ({
+ paper: {
+ padding: theme.spacing(2)
+ },
+ headingItem: {
+ marginRight: theme.spacing(1)
+ },
+ body: {
+ padding: theme.spacing(3)
+ }
+}));
+
+const FilterForm = ({ onSubmit, event }) => {
+ const classes = useStyles();
+ const [expanded, setExpanded] = useState(false);
+ const [filter, setFilter] = useState();
+ const [filterType, setFilterType] = useState();
+ const [filterValue, setFilterValue] = useState();
+
+ useEffect(() => {
+ setFilterType(undefined);
+ }, [filter]);
+
+ useEffect(() => {
+ setFilterValue(undefined);
+ }, [filterType]);
+
+ const toggleExpanded = useCallback(() => {
+ setExpanded(!expanded);
+ }, [expanded]);
+
+ const handleClear = useCallback(() => {
+ setExpanded(false);
+ setFilter(undefined);
+ setFilterType(undefined);
+ setFilterValue(undefined);
+ }, []);
+
+ const filterParams = useMemo(() => {
+ return filter ? JSON.parse(filter) : null;
+ }, [filter]);
+
+ const submitValue = useMemo(() => {
+ if (!filterParams) return null;
+ if (!filterType) return null;
+
+ return {
+ label: filterParams.label,
+ path: filterParams.path,
+ type: filterType,
+ value: filterValue
+ };
+ }, [filterParams, filterType, filterValue]);
+
+ const handleSubmit = useCallback(() => {
+ onSubmit(submitValue);
+ handleClear();
+ }, [submitValue, onSubmit, handleClear]);
+
+ const filterOptions = useMemo(() => {
+ return RegistrationFields.filters.map(filter => ({
+ value: JSON.stringify(filter),
+ label: filter.label
+ }));
+ }, []);
+
+ const filterTypeOptions = useMemo(() => {
+ if (!filterParams) return [];
+ const options = FilterTypes.filterTypesForType[filterParams.type];
+ if (!options) return [];
+
+ return options.map(option => ({
+ value: option,
+ label: FilterTypes.filterTypes[option].label
+ }));
+ }, [filterParams]);
+
+ return (
+
+ } aria-controls="panel1c-content" id="panel1c-header">
+
+ Add a filter
+
+
+
+
+
+
+
+
+ {filterTypeOptions.length > 0 && (
+
+ )}
+
+
+
+
+
+
+
+ Cancel
+
+ Add
+
+
+
+ );
+};
+
+const mapState = state => ({
+ event: OrganiserSelectors.event(state)
+});
+
+export default connect(mapState)(FilterForm);
diff --git a/frontend/src/components/filters/FilterGroupMenu.js b/frontend/src/components/filters/FilterGroupMenu.js
new file mode 100644
index 000000000..91cb4d41d
--- /dev/null
+++ b/frontend/src/components/filters/FilterGroupMenu.js
@@ -0,0 +1,152 @@
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
+
+import { makeStyles } from '@material-ui/core/styles';
+import { connect } from 'react-redux';
+import { sortBy } from 'lodash-es';
+import { List, ListItem, ListItemText, Menu, MenuItem, Paper, Box, Divider } from '@material-ui/core';
+
+import FilterForm from './FilterForm';
+import FilterList from './FilterList';
+import FilterSaveForm from './FilterSaveForm';
+
+import * as OrganiserSelectors from 'redux/organiser/selectors';
+
+const useStyles = makeStyles(theme => ({
+ root: {}
+}));
+
+const FilterGroupMenu = ({
+ onChange = () => {},
+ onSelectedChange = () => {},
+ event,
+ filterGroups,
+ showEdit = true
+}) => {
+ const classes = useStyles();
+ const [anchorEl, setAnchorEl] = React.useState(null);
+
+ const [selected, setSelected] = useState();
+ const [filters, setFilters] = useState([]);
+
+ useEffect(() => {
+ if (selected) {
+ setFilters(selected.filters);
+ } else {
+ setFilters([]);
+ }
+ }, [selected]);
+
+ useEffect(() => {
+ onSelectedChange(selected);
+ }, [selected, onSelectedChange]);
+
+ useEffect(() => {
+ onChange(filters);
+ }, [filters, onChange]);
+
+ const handleFilterAdd = useCallback(
+ filter => {
+ setFilters(filters.concat(filter));
+ },
+ [filters]
+ );
+
+ const handleClickListItem = event => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleMenuItemClick = option => {
+ if (option.isDefault) {
+ setSelected();
+ } else {
+ setSelected(option);
+ }
+ setAnchorEl(null);
+ };
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ const options = useMemo(() => {
+ let items = [
+ {
+ label: 'All participants',
+ description: 'No filters',
+ filters: [],
+ isDefault: true
+ }
+ ];
+
+ if (showEdit) {
+ items.push({
+ label: 'New filters',
+ description: 'Apply a set of custom filters',
+ filters: [],
+ isAdd: true
+ });
+ }
+
+ items = items.concat(sortBy(filterGroups, 'label'));
+
+ return items;
+ }, [filterGroups, showEdit]);
+
+ const activeItem = selected || options[0];
+ const reservedLabels = options.map(option => option.label);
+
+ return (
+
+
+
+
+
+
+
+ {showEdit && !activeItem.isDefault && (
+
+
+
+
+
+ )}
+
+ );
+};
+
+const mapState = state => ({
+ filterGroups: OrganiserSelectors.filterGroups(state),
+ event: OrganiserSelectors.event(state)
+});
+
+export default connect(mapState)(FilterGroupMenu);
diff --git a/frontend/src/components/filters/FilterList.js b/frontend/src/components/filters/FilterList.js
new file mode 100644
index 000000000..6abfcbe44
--- /dev/null
+++ b/frontend/src/components/filters/FilterList.js
@@ -0,0 +1,79 @@
+import React, { useState, useCallback, useEffect } from 'react';
+
+import { makeStyles } from '@material-ui/core/styles';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+
+import {
+ List,
+ Divider,
+ ExpansionPanel,
+ ExpansionPanelSummary,
+ ExpansionPanelDetails,
+ ExpansionPanelActions,
+ Typography,
+ Button,
+ Badge
+} from '@material-ui/core';
+import FilterListItem from 'components/filters/FilterListItem';
+
+const useStyles = makeStyles(theme => ({
+ headingItem: {
+ marginRight: theme.spacing(1)
+ },
+ badge: {
+ right: -1 * theme.spacing(2),
+ top: theme.spacing(1)
+ },
+ list: {
+ width: '100%'
+ }
+}));
+
+const FilterList = ({ activeItemKey, filters = [], onChange = () => {} }) => {
+ const classes = useStyles();
+ const [expanded, setExpanded] = useState(false);
+ const toggleExpanded = useCallback(() => setExpanded(!expanded), [expanded]);
+ const hasFilters = filters.length !== 0;
+
+ useEffect(() => {
+ setExpanded(false);
+ }, [activeItemKey]);
+
+ const handleRemove = useCallback(
+ index => {
+ const newFilters = filters.filter((filter, idx) => {
+ return idx !== index;
+ });
+ onChange(newFilters);
+ },
+ [onChange, filters]
+ );
+
+ return (
+
+ } aria-controls="panel1c-content" id="panel1c-header">
+
+ Active filters
+
+
+
+
+ {filters.map((filter, index) => (
+
+ {index !== 0 && }
+ handleRemove(index)} />
+
+ ))}
+
+
+
+ );
+};
+
+export default FilterList;
diff --git a/frontend/src/components/filters/FilterListItem.js b/frontend/src/components/filters/FilterListItem.js
new file mode 100644
index 000000000..825fe7a0a
--- /dev/null
+++ b/frontend/src/components/filters/FilterListItem.js
@@ -0,0 +1,66 @@
+import React from 'react';
+
+import DeleteIcon from '@material-ui/icons/Delete';
+import { makeStyles } from '@material-ui/core/styles';
+import { Typography, ListItem, ListItemText, ListItemSecondaryAction, IconButton, Chip } from '@material-ui/core';
+import { FilterTypes } from '@hackjunction/shared';
+
+const useStyles = makeStyles(theme => ({
+ inline: {
+ display: 'inline'
+ },
+ chips: {
+ display: 'flex',
+ flexDirection: 'row',
+ flexWrap: 'wrap'
+ },
+ chip: {
+ margin: 2
+ }
+}));
+
+const FilterListItem = ({ filter = {}, onRemove }) => {
+ const classes = useStyles();
+ const getType = () => {
+ const params = FilterTypes.filterTypes[filter.type];
+ return params ? params.label : filter.type;
+ };
+
+ const renderValue = (value) => {
+ if (Array.isArray(value)) {
+ return(
+
+ {value.map(item => (
+
+ ))}
+
+ )
+ }
+ return value;
+ }
+
+ return (
+
+
+
+ {getType()}
+ {' '}
+ {renderValue(filter.value)}
+
+ }
+ />
+ {typeof onRemove === 'function' && (
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default FilterListItem;
diff --git a/frontend/src/components/filters/FilterSaveForm.js b/frontend/src/components/filters/FilterSaveForm.js
new file mode 100644
index 000000000..3b2ffc76b
--- /dev/null
+++ b/frontend/src/components/filters/FilterSaveForm.js
@@ -0,0 +1,209 @@
+import React, { useState, useEffect, useCallback } from 'react';
+
+import { withSnackbar } from 'notistack';
+import { connect } from 'react-redux';
+import {
+ ExpansionPanel,
+ ExpansionPanelSummary,
+ ExpansionPanelDetails,
+ ExpansionPanelActions,
+ Typography,
+ Button,
+ Grid,
+ CircularProgress
+} from '@material-ui/core';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import TextInput from 'components/inputs/TextInput';
+import { useFormField } from 'hooks/formHooks';
+import * as OrganiserActions from 'redux/organiser/actions';
+import * as OrganiserSelectors from 'redux/organiser/selectors';
+
+const FilterSaveForm = ({
+ createFilterGroup,
+ editFilterGroup,
+ deleteFilterGroup,
+ filters,
+ activeItem,
+ reservedLabels,
+ event,
+ enqueueSnackbar,
+ onSave,
+ onDelete
+}) => {
+ const isEdit = !activeItem.isDefault && !activeItem.isAdd;
+ const [loading, setLoading] = useState(false);
+ const [expanded, setExpanded] = useState(false);
+ const label = useFormField(isEdit ? activeItem.label : '', value => {
+ if (value.length === 0) {
+ return 'Name is required';
+ }
+
+ if (value.length > 50) {
+ return 'Name must be under 50 characters';
+ }
+
+ if (!isEdit) {
+ if (reservedLabels.indexOf(value) !== -1) {
+ return 'Name is already taken';
+ }
+ }
+
+ return;
+ });
+
+ const description = useFormField(isEdit ? activeItem.description : '', value => {
+ if (value.length > 100) {
+ return 'Description must be under 100 characters';
+ }
+
+ return;
+ });
+
+ const toggleExpanded = useCallback(
+ (event, isExpanded) => {
+ setExpanded(isExpanded);
+ label.setValue(isEdit ? activeItem.label : '');
+ description.setValue(isEdit ? activeItem.description : '');
+ },
+ [isEdit, label, description, activeItem]
+ );
+
+ useEffect(() => {
+ setExpanded(false);
+ }, [activeItem]);
+
+ const handleSubmit = () => {
+ const errs = [label.validate(), description.validate()].filter(err => err !== undefined);
+ if (errs.length > 0) {
+ return;
+ }
+
+ if (isEdit) {
+ handleEdit(label.value, description.value);
+ } else {
+ handleCreate(label.value, description.value);
+ }
+ };
+
+ const handleEdit = useCallback(
+ (label, description) => {
+ setLoading(true);
+ editFilterGroup(event.slug, label, description, filters)
+ .then(item => {
+ enqueueSnackbar('Edits saved!', { variant: 'success' });
+ toggleExpanded(null, false);
+ onSave(item);
+ })
+ .catch(err => {
+ enqueueSnackbar('Something went wrong', { variant: 'error' });
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ },
+ [onSave, toggleExpanded, enqueueSnackbar, event.slug, filters, editFilterGroup]
+ );
+
+ const handleCreate = useCallback(
+ (label, description) => {
+ setLoading(true);
+ createFilterGroup(event.slug, label, description, filters)
+ .then(item => {
+ enqueueSnackbar('Filter group created', { variant: 'success' });
+ toggleExpanded(null, false);
+ onSave(item);
+ })
+ .catch(err => {
+ enqueueSnackbar('Something went wrong', { variant: 'error' });
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ },
+ [onSave, toggleExpanded, enqueueSnackbar, event.slug, filters, createFilterGroup]
+ );
+
+ const handleDelete = useCallback(() => {
+ setLoading(true);
+ deleteFilterGroup(event.slug, label.value)
+ .then(() => {
+ enqueueSnackbar('Filter group deleted', { variant: 'success' });
+ toggleExpanded(null, false);
+ onDelete();
+ })
+ .catch(err => {
+ enqueueSnackbar('Something went wrong', { variant: 'error' });
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ }, [label, onDelete, toggleExpanded, enqueueSnackbar, deleteFilterGroup, event.slug]);
+
+ return (
+
+ } aria-controls="save-filters" id="save-filters">
+ {isEdit ? 'Edit these filters' : 'Save these filters'}
+
+
+
+ {!isEdit && (
+
+
+ You can save this filter group for later use. This allows you to easily view stats for
+ the group, and do things like bulk edit their applications or send an email to everyone
+ in the group.
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ {loading && }
+ {isEdit && (
+
+ Delete filter group
+
+ )}
+
+ {isEdit ? 'Save edits' : 'Create new filter group'}
+
+
+
+ );
+};
+
+const mapState = state => ({
+ event: OrganiserSelectors.event(state)
+});
+
+const mapDispatch = dispatch => ({
+ createFilterGroup: (slug, label, description, filters) =>
+ dispatch(OrganiserActions.createFilterGroup(slug, label, description, filters)),
+ editFilterGroup: (slug, label, description, filters) =>
+ dispatch(OrganiserActions.editFilterGroup(slug, label, description, filters)),
+ deleteFilterGroup: (slug, label) => dispatch(OrganiserActions.deleteFilterGroup(slug, label))
+});
+
+export default withSnackbar(
+ connect(
+ mapState,
+ mapDispatch
+ )(FilterSaveForm)
+);
diff --git a/frontend/src/components/filters/FilterValueInput.js b/frontend/src/components/filters/FilterValueInput.js
new file mode 100644
index 000000000..9cad0dbb3
--- /dev/null
+++ b/frontend/src/components/filters/FilterValueInput.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import { FilterTypes, FilterValues } from '@hackjunction/shared';
+
+import TextInput from 'components/inputs/TextInput';
+import Select from 'components/inputs/Select';
+
+const MULTI_TYPES = [
+ FilterTypes.filterTypes.ONE_OF.id,
+ FilterTypes.filterTypes.NOT_ONE_OF.id,
+ FilterTypes.filterTypes.CONTAINS_ONE_OF.id,
+ FilterTypes.filterTypes.NOT_CONTAINS_ONE_OF.id
+];
+
+const FilterValueInput = ({ filterType, valueType, value, onChange, event }) => {
+ const inputParams = { value, onChange };
+ switch (filterType) {
+ case FilterTypes.filterTypes.LESS_THAN.id:
+ case FilterTypes.filterTypes.NOT_LESS_THAN.id:
+ case FilterTypes.filterTypes.MORE_THAN.id:
+ case FilterTypes.filterTypes.NOT_MORE_THAN.id:
+ return (
+
+ );
+ case FilterTypes.filterTypes.CONTAINS.id:
+ case FilterTypes.filterTypes.NOT_CONTAINS.id:
+ case FilterTypes.filterTypes.EQUALS.id:
+ case FilterTypes.filterTypes.NOT_EQUALS.id:
+ case FilterTypes.filterTypes.ONE_OF.id:
+ case FilterTypes.filterTypes.NOT_ONE_OF.id:
+ case FilterTypes.filterTypes.CONTAINS_ONE_OF.id:
+ case FilterTypes.filterTypes.NOT_CONTAINS_ONE_OF.id:
+ const isMulti = MULTI_TYPES.indexOf(filterType) !== -1;
+ switch (valueType) {
+ case FilterValues.STRING:
+ return
;
+ case FilterValues.BOOLEAN:
+ return
;
+ case FilterValues.DATE:
+ return
;
+ case FilterValues.GENDER:
+ return
;
+ case FilterValues.NATIONALITY:
+ return
;
+ case FilterValues.COUNTRY:
+ return
;
+ case FilterValues.LANGUAGE:
+ return
;
+ case FilterValues.TAG:
+ const options = event.tags.map(tag => ({
+ value: tag.label,
+ label: tag.label
+ }));
+ return
;
+ case FilterValues.STATUS:
+ return
;
+ default:
+ return null;
+ }
+ case FilterTypes.filterTypes.IS_EMPTY:
+ case FilterTypes.filterTypes.NOT_EMPTY:
+ case FilterTypes.filterTypes.BOOLEAN_FALSE.id:
+ case FilterTypes.filterTypes.BOOLEAN_TRUE.id:
+ return null;
+ default:
+ return null;
+ }
+};
+
+export default FilterValueInput;
diff --git a/frontend/src/components/generic/ActionMenu/index.js b/frontend/src/components/generic/ActionMenu/index.js
new file mode 100644
index 000000000..4806b2fbe
--- /dev/null
+++ b/frontend/src/components/generic/ActionMenu/index.js
@@ -0,0 +1,57 @@
+import React, { useState, useCallback } from 'react';
+
+import { Menu, MenuItem, IconButton, Tooltip } from '@material-ui/core';
+import MoreHorizIcon from '@material-ui/icons/MoreHoriz';
+
+const ActionMenu = ({ title = 'Actions', actions = [], actionProps }) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+
+ const handleClick = useCallback(event => {
+ setAnchorEl(event.currentTarget);
+ }, []);
+
+ const handleClose = useCallback(event => {
+ setAnchorEl(null);
+ }, []);
+
+ const handleItemClick = action => {
+ action.action(...actionProps);
+ handleClose();
+ };
+
+ if (actions.length === 1) {
+ const action = actions[0];
+ return (
+
+ handleItemClick(action)}>
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ActionMenu;
diff --git a/frontend/src/components/generic/Empty/index.js b/frontend/src/components/generic/Empty/index.js
new file mode 100644
index 000000000..28152df5c
--- /dev/null
+++ b/frontend/src/components/generic/Empty/index.js
@@ -0,0 +1,55 @@
+import React from 'react';
+
+import { makeStyles } from '@material-ui/core/styles';
+import { Box, Button } from '@material-ui/core';
+import InfoTwoToneIcon from '@material-ui/icons/InfoTwoTone';
+import Typography from 'antd/lib/typography/Typography';
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ backgroundColor: '#efefef'
+ }
+}));
+
+const Empty = ({ isEmpty, emptyText = 'No data', button, hideIfNotEmpty = false }) => {
+ const classes = useStyles();
+
+ const renderButton = () => {
+ if (!button) return null;
+ return (
+
+ {button.text}
+
+ );
+ };
+
+ if (!isEmpty && !hideIfNotEmpty) {
+ return (
+
+ {renderButton()}
+
+ );
+ }
+
+ return (
+
+
+
+ {emptyText}
+
+ {button && {renderButton()} }
+
+ );
+};
+
+export default Empty;
diff --git a/frontend/src/components/generic/List/index.js b/frontend/src/components/generic/List/index.js
new file mode 100644
index 000000000..04104971b
--- /dev/null
+++ b/frontend/src/components/generic/List/index.js
@@ -0,0 +1,50 @@
+import React from 'react';
+
+import objectPath from 'object-path';
+import DeleteIcon from '@material-ui/icons/Delete';
+import { makeStyles } from '@material-ui/core/styles';
+
+import { List, ListItem, ListItemText, ListItemSecondaryAction, IconButton, Divider } from '@material-ui/core';
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ padding: theme.spacing(2)
+ }
+}));
+
+const _List = ({
+ data = [],
+ rowKey,
+ renderPrimary = () => {},
+ renderSecondary = () => {},
+ emptyView = null,
+ onDelete,
+ hasDividers = true
+}) => {
+ const classes = useStyles();
+ if (data.length === 0) {
+ return emptyView;
+ }
+
+ return (
+
+ {data.map((item, index) => (
+
+ {index !== 0 && }
+
+
+ {onDelete && (
+
+ onDelete(item, index)}>
+
+
+
+ )}
+
+
+ ))}
+
+ );
+};
+
+export default _List;
diff --git a/frontend/src/components/generic/Modal/index.js b/frontend/src/components/generic/Modal/index.js
new file mode 100644
index 000000000..db59febad
--- /dev/null
+++ b/frontend/src/components/generic/Modal/index.js
@@ -0,0 +1,70 @@
+import React from 'react';
+
+import HyperModal from 'react-hyper-modal';
+import classNames from 'classnames';
+import { Box, Typography } from '@material-ui/core';
+import { makeStyles } from '@material-ui/core/styles';
+
+const useStyles = makeStyles(theme => ({
+ wrapper: {
+ display: 'flex',
+ zIndex: 100
+ },
+ wrapperPadded: {
+ padding: theme.spacing(2)
+ },
+ content: {
+ background: '#ffffff',
+ width: '100% !important',
+ maxWidth: '600px',
+ zIndex: 1,
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'stretch'
+ },
+ contentMed: {
+ maxWidth: '900px'
+ },
+ contentMax: {
+ maxWidth: 'none',
+ height: '100% !important',
+ borderRadius: '0 !important'
+ },
+ header: {
+ padding: theme.spacing(3),
+ textAlign: 'left'
+ },
+ inner: {
+ padding: '1rem',
+ flex: 1,
+ overflow: 'auto'
+ }
+}));
+
+const GenericModal = ({ title, isOpen, handleClose, size, children }) => {
+ const classes = useStyles();
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+};
+
+export default GenericModal;
diff --git a/frontend/src/components/generic/PageHeader/index.js b/frontend/src/components/generic/PageHeader/index.js
new file mode 100644
index 000000000..c1ccbb046
--- /dev/null
+++ b/frontend/src/components/generic/PageHeader/index.js
@@ -0,0 +1,28 @@
+import React from 'react';
+
+import { makeStyles } from '@material-ui/core/styles';
+import { Box, Typography } from '@material-ui/core';
+
+const useStyles = makeStyles(theme => ({
+ subheading: {
+ marginTop: theme.spacing(1),
+ marginLeft: theme.spacing(0.5),
+ fontFamily: 'Lato'
+ }
+}));
+
+const PageHeader = ({ heading, subheading }) => {
+ const classes = useStyles();
+ return (
+
+ {heading}
+ {subheading && (
+
+ {subheading}
+
+ )}
+
+ );
+};
+
+export default PageHeader;
diff --git a/frontend/src/components/generic/Statistic/index.js b/frontend/src/components/generic/Statistic/index.js
new file mode 100644
index 000000000..7e928ec5a
--- /dev/null
+++ b/frontend/src/components/generic/Statistic/index.js
@@ -0,0 +1,52 @@
+import React, { useState, useCallback } from 'react';
+
+import { makeStyles } from '@material-ui/core/styles';
+import { Card, CardContent, Typography, Button, Box, CircularProgress } from '@material-ui/core';
+
+const useStyles = makeStyles(theme => ({
+ value: {
+ textAlign: 'left'
+ },
+ suffix: {
+ marginLeft: theme.spacing(1),
+ display: 'inline-block'
+ }
+}));
+
+const Statistic = ({ label, value, suffix, action, actionText }) => {
+ const [actionLoading, setActionLoading] = useState();
+
+ const handleAction = useCallback(async () => {
+ setActionLoading(true);
+ await action();
+ setActionLoading(false);
+ }, [action]);
+ const classes = useStyles();
+ return (
+
+
+
+ {label}
+
+
+ {value}
+ {suffix && (
+
+ {suffix}
+
+ )}
+
+ {action && actionText && (
+
+ {actionLoading && }
+
+ {actionText}
+
+
+ )}
+
+
+ );
+};
+
+export default Statistic;
diff --git a/frontend/src/components/generic/Stepper/index.js b/frontend/src/components/generic/Stepper/index.js
new file mode 100644
index 000000000..7e068632d
--- /dev/null
+++ b/frontend/src/components/generic/Stepper/index.js
@@ -0,0 +1,47 @@
+import React, { useCallback } from 'react';
+
+import { Stepper, Step, StepLabel, StepContent } from '@material-ui/core';
+
+import StepButtons from 'components/buttons/StepButtons';
+
+const _Stepper = ({
+ steps = [],
+ activeStep = 0,
+ onStepChange,
+ onFinish,
+ stepperProps = { orientation: 'vertical' }
+}) => {
+ const handleNext = useCallback(() => {
+ onStepChange(activeStep + 1);
+ }, [onStepChange, activeStep]);
+
+ const handleBack = useCallback(() => {
+ onStepChange(activeStep - 1);
+ }, [onStepChange, activeStep]);
+
+ const handleDone = useCallback(() => {
+ onFinish();
+ }, [onFinish]);
+
+ return (
+
+ {steps.map((step, index) => (
+
+ {step.label}
+
+ {step.render()}
+
+
+
+ ))}
+
+ );
+};
+
+export default _Stepper;
diff --git a/frontend/src/components/generic/Table/TablePaginationActions.js b/frontend/src/components/generic/Table/TablePaginationActions.js
new file mode 100644
index 000000000..e0972862c
--- /dev/null
+++ b/frontend/src/components/generic/Table/TablePaginationActions.js
@@ -0,0 +1,65 @@
+import React, { useCallback } from 'react';
+
+import { IconButton, Box } from '@material-ui/core';
+
+import FirstPageIcon from '@material-ui/icons/FirstPage';
+import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
+import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
+import LastPageIcon from '@material-ui/icons/LastPage';
+
+const TablePaginationActions = ({ count, page, rowsPerPage, onChangePage }) => {
+ const handleFirstPageButtonClick = useCallback(
+ event => {
+ onChangePage(event, 0);
+ },
+ [onChangePage]
+ );
+
+ const handleBackButtonClick = useCallback(
+ event => {
+ onChangePage(event, page - 1);
+ },
+ [onChangePage, page]
+ );
+
+ const handleNextButtonClick = useCallback(
+ event => {
+ onChangePage(event, page + 1);
+ },
+ [onChangePage, page]
+ );
+
+ const handleLastPageButtonClick = useCallback(
+ event => {
+ onChangePage(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
+ },
+ [onChangePage, count, rowsPerPage]
+ );
+
+ return (
+
+
+
+
+
+
+
+ = Math.ceil(count / rowsPerPage) - 1}
+ aria-label="next page"
+ >
+
+
+ = Math.ceil(count / rowsPerPage) - 1}
+ aria-label="last page"
+ >
+
+
+
+ );
+};
+
+export default TablePaginationActions;
diff --git a/frontend/src/components/generic/Table/TableToolbar.js b/frontend/src/components/generic/Table/TableToolbar.js
new file mode 100644
index 000000000..a55bb2820
--- /dev/null
+++ b/frontend/src/components/generic/Table/TableToolbar.js
@@ -0,0 +1,57 @@
+import React from 'react';
+
+import { lighten, makeStyles } from '@material-ui/core/styles';
+import { Toolbar, Tooltip, Typography, Box, IconButton } from '@material-ui/core';
+
+import classNames from 'classnames';
+
+const useToolbarStyles = makeStyles(theme => ({
+ root: {
+ paddingLeft: theme.spacing(2),
+ paddingRight: theme.spacing(1),
+ transition: 'all 0.2s ease'
+ },
+ highlight: {
+ color: theme.palette.primary.dark,
+ backgroundColor: lighten(theme.palette.primary.main, 0.85)
+ },
+ action: {
+ transform: 'scale(0.5)',
+ transition: 'all 0.2s ease',
+ opacity: 0,
+ pointerEvents: 'none'
+ },
+ actionActive: {
+ transform: 'scale(1)',
+ opacity: 1,
+ pointerEvents: 'initial'
+ }
+}));
+
+const TableToolbar = ({ title, selectedRows, actions = [] }) => {
+ const classes = useToolbarStyles();
+ const hasSelected = selectedRows.length > 0;
+
+ return (
+
+
+ {hasSelected ? `${selectedRows.length} selected` : title}
+
+
+ {actions.map(action => (
+
+ action.action(selectedRows)} aria-label={action.label}>
+ {action.icon}
+
+
+ ))}
+
+
+ );
+};
+
+export default TableToolbar;
diff --git a/frontend/src/components/generic/Table/index.js b/frontend/src/components/generic/Table/index.js
new file mode 100644
index 000000000..07b2dd8fa
--- /dev/null
+++ b/frontend/src/components/generic/Table/index.js
@@ -0,0 +1,214 @@
+import React, { useState, useCallback, useMemo } from 'react';
+
+import { lighten, makeStyles } from '@material-ui/core/styles';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableRow,
+ TableHead,
+ TableFooter,
+ TablePagination,
+ Box,
+ Paper,
+ CircularProgress,
+ Checkbox
+} from '@material-ui/core';
+
+import objectPath from 'object-path';
+
+import TableToolbar from './TableToolbar';
+import TablePaginationActions from './TablePaginationActions';
+import ActionMenu from 'components/generic/ActionMenu';
+
+const useTableStyles = makeStyles(theme => ({
+ root: {
+ overflowX: 'auto'
+ }
+}));
+
+const usePaginationStyles = makeStyles(theme => ({
+ toolbar: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ height: 'auto'
+ }
+}));
+
+export default ({
+ columns = [],
+ dataSource = [],
+ rowKey,
+ loading = true,
+ pagination = true,
+ rowNumber = true,
+ rowSelection = true,
+ footer,
+ title = '',
+ selectedActions = [],
+ rowActions = []
+}) => {
+ const classes = useTableStyles();
+ const paginationClasses = usePaginationStyles();
+ const [rowsPerPage, setRowsPerPage] = useState(10);
+ const [page, setPage] = useState(0);
+ const [selectedRows, setSelectedRows] = useState([]);
+
+ const handleChangePage = useCallback((event, newPage) => {
+ setPage(newPage);
+ }, []);
+
+ const handleChangeRowsPerPage = useCallback(event => {
+ setRowsPerPage(parseInt(event.target.value, 10));
+ setPage(0);
+ }, []);
+
+ const handleSelectAll = useCallback(() => {
+ if (selectedRows.length === dataSource.length) {
+ setSelectedRows([]);
+ } else {
+ setSelectedRows(dataSource.map(item => item[rowKey]));
+ }
+ }, [dataSource, rowKey, selectedRows]);
+
+ const handleSelectRow = item => {
+ const key = item[rowKey];
+ let rows = selectedRows.slice();
+ const index = rows.indexOf(key);
+ if (rows.indexOf(key) !== -1) {
+ rows.splice(index, 1);
+ } else {
+ rows = rows.concat(key);
+ }
+ setSelectedRows(rows);
+ };
+
+ const isRowSelected = item => {
+ return selectedRows.indexOf(item[rowKey]) !== -1;
+ };
+
+ const columnCount = useMemo(() => {
+ let columnCount = columns.length;
+ if (rowSelection) columnCount++;
+ if (rowNumber) columnCount++;
+ if (rowActions) columnCount++;
+ return columnCount;
+ }, [columns, rowSelection, rowActions, rowNumber]);
+
+ const data = useMemo(() => {
+ return dataSource.slice(page * rowsPerPage, (page + 1) * rowsPerPage);
+ }, [dataSource, page, rowsPerPage]);
+
+ const isAllSelected = useMemo(() => {
+ if (!dataSource.length || loading) return false;
+ return selectedRows.length === dataSource.length;
+ }, [selectedRows, dataSource, loading]);
+
+ return (
+
+
+
+
+
+
+ {rowSelection && (
+
+
+
+ )}
+ {rowNumber && # }
+ {rowActions.length > 0 && (
+
+ Actions
+
+ )}
+ {columns.map(column => (
+ {column.label}
+ ))}
+
+
+
+ {loading && (
+
+
+
+
+
+ )}
+ {!loading && data.length === 0 && (
+
+
+ No data
+
+
+ )}
+ {!loading &&
+ data.length > 0 &&
+ data.map((item, index) => (
+
+ {rowSelection && (
+
+ handleSelectRow(item)}
+ />
+
+ )}
+ {rowNumber && (
+ {1 + index + page * rowsPerPage}
+ )}
+ {rowActions.length > 0 && (
+
+
+
+ )}
+ {columns.map(column => {
+ const value = objectPath.get(item, column.path);
+ return (
+
+ {column.render ? column.render(value, item) : value}
+
+ );
+ })}
+
+ ))}
+
+ {footer && (
+
+
+
+ {footer}
+
+
+
+ )}
+
+
+ {pagination && (
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/inputs/Select/index.js b/frontend/src/components/inputs/Select/index.js
new file mode 100644
index 000000000..1a4236b10
--- /dev/null
+++ b/frontend/src/components/inputs/Select/index.js
@@ -0,0 +1,95 @@
+import React, { useCallback, useMemo } from 'react';
+
+import { makeStyles } from '@material-ui/core/styles';
+import { TextField, MenuItem, Chip } from '@material-ui/core';
+import { SelectOptions } from '@hackjunction/shared';
+
+const useStyles = makeStyles(theme => ({
+ chips: {
+ display: 'flex',
+ flexWrap: 'wrap'
+ },
+ chip: {
+ margin: 2,
+ }
+}));
+
+const _Select = ({
+ label,
+ placeholder,
+ helperText,
+ value,
+ onChange = () => {},
+ options = [],
+ type,
+ multiple = false
+}) => {
+ const classes = useStyles()
+ const handleChange = useCallback(
+ e => {
+ onChange(e.target.value);
+ },
+ [onChange]
+ );
+
+ const items = useMemo(() => {
+ switch (type) {
+ case 'country':
+ return SelectOptions.COUNTRIES;
+ case 'nationality':
+ return SelectOptions.NATIONALITIES;
+ case 'gender':
+ return SelectOptions.GENDERS;
+ case 'industry':
+ return SelectOptions.INDUSTRY;
+ case 'language':
+ return SelectOptions.LANGUAGES;
+ case 'role':
+ return SelectOptions.ROLES;
+ case 'skill':
+ return SelectOptions.SKILLS;
+ case 'theme':
+ return SelectOptions.THEMES;
+ case 'status':
+ return SelectOptions.STATUSES;
+ default:
+ return options;
+ }
+ }, [options, type]);
+
+ const valueOrDefault = value || (multiple ? [] : '');
+
+ const selectProps = { multiple };
+ if (multiple) {
+ selectProps.renderValue = (value = []) => {
+ return(
+
+ {value.map(item => (
+
+ ))}
+
+ );
+ }
+ }
+
+ return (
+
+ {items.map(item => (
+
+ {item.label}
+
+ ))}
+
+ );
+};
+
+export default _Select;
diff --git a/frontend/src/components/inputs/TextInput/index.js b/frontend/src/components/inputs/TextInput/index.js
new file mode 100644
index 000000000..cd7dffcff
--- /dev/null
+++ b/frontend/src/components/inputs/TextInput/index.js
@@ -0,0 +1,20 @@
+import React, { useCallback } from 'react';
+
+import { TextField } from '@material-ui/core';
+
+const TextInput = ({ label, helperText, value = '', onChange = () => {}, error, disabled, rawOnChange = false, type = 'text', multiline = false }) => {
+ const handleChange = useCallback(
+ e => {
+ if (rawOnChange) {
+ onChange(e);
+ } else {
+ onChange(e.target.value);
+ }
+ },
+ [onChange, rawOnChange]
+ );
+
+ return
;
+};
+
+export default TextInput;
diff --git a/frontend/src/components/layouts/MaterialTabsLayout/index.js b/frontend/src/components/layouts/MaterialTabsLayout/index.js
new file mode 100644
index 000000000..e860b5242
--- /dev/null
+++ b/frontend/src/components/layouts/MaterialTabsLayout/index.js
@@ -0,0 +1,86 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { makeStyles, useTheme } from '@material-ui/core/styles';
+import { AppBar, Tabs, Tab, Typography, Box, useMediaQuery} from '@material-ui/core';
+
+function TabPanel(props) {
+ const { children, value, index, ...other } = props;
+
+ return (
+
+ {children}
+
+ );
+}
+
+TabPanel.propTypes = {
+ children: PropTypes.node,
+ index: PropTypes.any.isRequired,
+ value: PropTypes.any.isRequired,
+};
+
+function a11yProps(index) {
+ return {
+ id: `scrollable-auto-tab-${index}`,
+ 'aria-controls': `scrollable-auto-tabpanel-${index}`,
+ };
+}
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ flexGrow: 1,
+ width: '100%',
+ backgroundColor: theme.palette.background.paper,
+ },
+ wrapper: {
+ textAlign: 'left',
+ alignItems: 'flex-start'
+ }
+}));
+
+const MaterialTabsLayout = ({ tabs }) => {
+ const classes = useStyles();
+ const [value, setValue] = React.useState(0);
+
+ const handleChange = (event, newValue) => {
+ setValue(newValue);
+ };
+
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
+
+ return (
+
+
+ {tabs.map((tab, index) => (
+
+ ))}
+
+
+ {tabs.map((tab, index) => (
+
+ {tab.content}
+
+ ))}
+
+
+ );
+}
+
+export default MaterialTabsLayout;
\ No newline at end of file
diff --git a/frontend/src/components/layouts/SidebarLayout/index.js b/frontend/src/components/layouts/SidebarLayout/index.js
index 451b03920..53e3671bc 100644
--- a/frontend/src/components/layouts/SidebarLayout/index.js
+++ b/frontend/src/components/layouts/SidebarLayout/index.js
@@ -118,7 +118,11 @@ const SidebarLayout = React.memo(({ renderTop, renderSidebarTop, baseRoute, loca
if (hidden) {
return null;
} else {
- return
;
+ return (
+
+ {render()}
+
+ );
}
})}
diff --git a/frontend/src/components/modals/EditRegistrationModal/index.js b/frontend/src/components/modals/EditRegistrationModal/index.js
new file mode 100644
index 000000000..bdf5d713d
--- /dev/null
+++ b/frontend/src/components/modals/EditRegistrationModal/index.js
@@ -0,0 +1,270 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { connect } from 'react-redux';
+import Modal from 'components/generic/Modal';
+import { Typography } from '@material-ui/core';
+import { withSnackbar } from 'notistack';
+import { RegistrationFields } from '@hackjunction/shared';
+import { Rate, notification, Divider as AntDivider, Tag, Drawer, List, Select, Button as AntButton, Input } from 'antd';
+import { isEqual, groupBy, find } from 'lodash-es';
+
+import PageWrapper from 'components/PageWrapper';
+import Button from 'components/generic/Button';
+import Divider from 'components/generic/Divider';
+import DescriptionItem from 'components/generic/DescriptionItem';
+
+import UserSelectModal from 'components/modals/UserSelectModal';
+import RegistrationStatusSelect from 'components/FormComponents/RegistrationStatusSelect';
+
+import * as AuthSelectors from 'redux/auth/selectors';
+import * as OrganiserSelectors from 'redux/organiser/selectors';
+import * as OrganiserActions from 'redux/organiser/actions';
+
+import RegistrationsService from 'services/registrations';
+import MiscUtils from 'utils/misc';
+
+const EditRegistrationModalInner = ({ idToken, event, registration, organisers, organisersMap, onEdit }) => {
+ const initialValues = {
+ rating: registration.rating,
+ assignedTo: registration.assignedTo,
+ tags: registration.tags,
+ status: registration.status,
+ travelGrant: registration.travelGrant
+ };
+ const [formValues, setFormValues] = useState(initialValues);
+ const dirty = !isEqual(formValues, initialValues);
+
+ const handleEdit = (field, value) => {
+ setFormValues({
+ ...formValues,
+ [field]: value
+ });
+ };
+
+ const renderAssignedTo = () => {
+ if (formValues.assignedTo) {
+ const user = organisersMap[formValues.assignedTo];
+ return user ? `${user.firstName} ${user.lastName}` : '???';
+ }
+ return 'No one';
+ };
+
+ const renderActions = () => {
+ return (
+
+
+ handleEdit('rating', value)} />}
+ >
+
+ (
+
+ )}
+ onDone={value => handleEdit('assignedTo', value.userId)}
+ allowMultiple={false}
+ userProfiles={organisers}
+ />
+ ]}
+ >
+
+
+
+ handleEdit('tags', value)}
+ mode="multiple"
+ >
+ {event.tags.map(tag => (
+
+ {tag.label}
+
+ ))}
+
+ }
+ >
+
+
+ handleEdit('status', value)}
+ />
+ }
+ />
+
+
+ handleEdit('travelGrant', e.target.value)} />
+ }
+ />
+
+
+ onEdit(formValues),
+ disabled: !dirty
+ }}
+ />
+
+ );
+ };
+
+ const renderContent = () => {
+ if (!registration) return null;
+ const fields = Object.keys(registration.answers);
+ const grouped = groupBy(fields, field => RegistrationFields.getCategory(field));
+ const categoryNames = Object.keys(grouped).filter(key => key !== '');
+
+ return (
+
+ {categoryNames.map(name => (
+
+
+ {name}
+
+ {grouped[name].map(field => {
+ let label = RegistrationFields.fieldToLabelMap[field];
+ if (!label) {
+ const customField = find(event.registrationQuestions, f => f.name === field);
+ if (customField) {
+ label = customField.label;
+ }
+ }
+ return (
+
+ );
+ })}
+
+ ))}
+ {event.customQuestions.map(section => {
+ const sectionAnswers = registration.answers[section.name] || {};
+ return (
+
+
+ {section.label}
+
+ {section.questions.map(question => {
+ return (
+
+ );
+ })}
+
+ );
+ })}
+
+ );
+ };
+
+ return (
+
+ {renderActions()}
+ {renderContent()}
+
+ );
+};
+
+const EditRegistrationModal = ({
+ idToken,
+ registrationId,
+ onClose,
+ event,
+ editRegistration,
+ enqueueSnackbar,
+ organisers,
+ organisersMap,
+ onEdited = () => {}
+}) => {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(false);
+ const [registration, setRegistration] = useState();
+ const { slug } = event;
+
+ useEffect(() => {
+ if (registrationId) {
+ setLoading(true);
+ RegistrationsService.getFullRegistration(idToken, slug, registrationId)
+ .then(data => {
+ setRegistration(data);
+ })
+ .catch(err => {
+ setError(true);
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ }
+ }, [idToken, registrationId, slug]);
+
+ const handleEdit = useCallback(
+ async data => {
+ setLoading(true);
+ await MiscUtils.sleep(1000);
+ editRegistration(registrationId, data, slug)
+ .then(data => {
+ enqueueSnackbar('Changes saved!', { variant: 'success' });
+ onEdited(data);
+ onClose();
+ })
+ .catch(err => {
+ enqueueSnackbar('Something went wrong', { variant: 'error' });
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ },
+ [enqueueSnackbar, editRegistration, registrationId, slug, onClose, onEdited]
+ );
+
+ return (
+
+
+
+
+
+ );
+};
+
+const mapState = state => ({
+ idToken: AuthSelectors.getIdToken(state),
+ event: OrganiserSelectors.event(state),
+ organisersMap: OrganiserSelectors.organisersMap(state),
+ organisers: OrganiserSelectors.organisers(state)
+});
+
+const mapDispatch = dispatch => ({
+ editRegistration: (registrationId, data, slug) =>
+ dispatch(OrganiserActions.editRegistration(registrationId, data, slug))
+});
+
+export default withSnackbar(
+ connect(
+ mapState,
+ mapDispatch
+ )(EditRegistrationModal)
+);
diff --git a/frontend/src/components/modals/VisaInvitationDrawer/VisaInvitationPDF.js b/frontend/src/components/modals/VisaInvitationDrawer/VisaInvitationPDF.js
index af54d4317..738a8518f 100644
--- a/frontend/src/components/modals/VisaInvitationDrawer/VisaInvitationPDF.js
+++ b/frontend/src/components/modals/VisaInvitationDrawer/VisaInvitationPDF.js
@@ -92,7 +92,6 @@ const VisaInvitationPDF = ({
know {arrivalCity} at their leisure and attend complementary events such as those organised by Slush
during the week after, November 18th to 25th.
-
Sincerely,
{hostName}
diff --git a/frontend/src/components/tables/AttendeeTable/index.js b/frontend/src/components/tables/AttendeeTable/index.js
index 1ad938cb1..0e5327a52 100644
--- a/frontend/src/components/tables/AttendeeTable/index.js
+++ b/frontend/src/components/tables/AttendeeTable/index.js
@@ -1,115 +1,126 @@
-import React from 'react';
+import React, { useState } from 'react';
import moment from 'moment';
-import { Table, Empty, Tag, Divider as AntDivider } from 'antd';
+import { Empty, Tag } from 'antd';
import { connect } from 'react-redux';
import { RegistrationStatuses } from '@hackjunction/shared';
+import EmailIcon from '@material-ui/icons/Email';
+import EditIcon from '@material-ui/icons/Edit';
+import Table from 'components/generic/Table';
+
import * as OrganiserSelectors from 'redux/organiser/selectors';
-import EditRegistrationDrawer from 'components/modals/EditRegistrationDrawer';
+import EditRegistrationModal from 'components/modals/EditRegistrationModal';
const AttendeeTable = ({ organiserProfilesMap, emptyRenderer, event, loading, attendees = [], footer = null }) => {
- const renderTotal = (total, range) => {
- return `${range[0]}-${range[1]} of ${total}`;
- };
+ const [editing, setEditing] = useState();
const renderTable = () => {
if (!loading) {
if (!Array.isArray(attendees) || attendees.length === 0) return null;
}
+
return (
- {
- return `${answers.firstName} ${answers.lastName}`;
- }}
- />
-
- rating || 'Pending'}
- />
- {
- const params = RegistrationStatuses.asObject[status];
- if (!params) return '-';
- return {params.label} ;
- }}
- />
- {
- if (!tags || !tags.length) {
- return '-';
- } else {
- return event.tags
- .filter(tag => {
- return tags.indexOf(tag.label) !== -1;
- })
- .map(({ color, label }) => (
-
- {label}
-
- ));
+ loading={loading}
+ title={`${attendees.length} results`}
+ footer={footer}
+ selectedActions={[
+ {
+ key: 'edit',
+ label: 'Edit all',
+ icon: ,
+ action: items => window.alert('Bulk edit temporarily unavailable')
+ },
+ {
+ key: 'email',
+ label: 'Email all',
+ icon: ,
+ action: items => window.alert('Bulk email temporarily unavailable')
+ }
+ ]}
+ rowActions={[
+ {
+ key: 'edit',
+ label: 'Edit',
+ action: item => setEditing(item._id)
+ }
+ ]}
+ columns={[
+ {
+ key: 'name',
+ path: 'answers',
+ label: 'Name',
+ render: answers => `${answers.firstName} ${answers.lastName}`
+ },
+ {
+ key: 'email',
+ path: 'answers.email',
+ label: 'Email'
+ },
+ {
+ key: 'rating',
+ path: 'rating',
+ label: 'Rating',
+ render: rating => rating || 'Pending'
+ },
+ {
+ key: 'status',
+ path: 'status',
+ label: 'Status',
+ render: status => {
+ const params = RegistrationStatuses.asObject[status];
+ if (!params) return '-';
+ return {params.label} ;
+ }
+ },
+ {
+ key: 'tags',
+ path: 'tags',
+ label: 'Tags',
+ render: tags => {
+ if (!tags || !tags.length) {
+ return '-';
+ } else {
+ return event.tags
+ .filter(tag => {
+ return tags.indexOf(tag.label) !== -1;
+ })
+ .map(({ color, label }) => (
+
+ {label}
+
+ ));
+ }
}
- }}
- />
- moment(date).fromNow()}
- />
- {
- let text;
- if (!userId) {
- text = 'No one';
- } else if (organiserProfilesMap.hasOwnProperty(userId)) {
- const user = organiserProfilesMap[userId];
- text = `${user.firstName} ${user.lastName}`;
- } else {
- text = 'Unknown user';
+ },
+ {
+ key: 'createdAt',
+ path: 'createdAt',
+ label: 'Submitted',
+ render: date => moment(date).fromNow()
+ },
+ {
+ key: 'assignedTo',
+ path: 'assignedTo',
+ label: 'Assigned to',
+ render: (userId, record) => {
+ let text;
+ if (!userId) {
+ text = 'No one';
+ } else if (organiserProfilesMap.hasOwnProperty(userId)) {
+ const user = organiserProfilesMap[userId];
+ text = `${user.firstName} ${user.lastName}`;
+ } else {
+ text = 'Unknown user';
+ }
+ return text;
}
- return text;
- }}
- />
- {
- return ;
- }}
- />
-
+ }
+ ]}
+ />
);
};
@@ -122,6 +133,7 @@ const AttendeeTable = ({ organiserProfilesMap, emptyRenderer, event, loading, at
return (
+
{renderTable()}
{renderEmpty()}
diff --git a/frontend/src/hooks/formHooks.js b/frontend/src/hooks/formHooks.js
index 899b3240b..ea5b70a7f 100644
--- a/frontend/src/hooks/formHooks.js
+++ b/frontend/src/hooks/formHooks.js
@@ -26,6 +26,17 @@ export const useFormField = (initialValue, validate = () => null, initialError =
setError(undefined);
}, []);
+ const handleValidate = useCallback(() => {
+ const err = validate(value);
+ if (err) {
+ setError(err);
+ return err;
+ } else {
+ setError();
+ return;
+ }
+ }, [value, validate]);
+
return {
value,
setValue,
@@ -33,6 +44,6 @@ export const useFormField = (initialValue, validate = () => null, initialError =
reset,
error,
setError,
- validate
+ validate: handleValidate
};
};
diff --git a/frontend/src/index.js b/frontend/src/index.js
index 8c673a36e..db35b5790 100755
--- a/frontend/src/index.js
+++ b/frontend/src/index.js
@@ -4,12 +4,15 @@ import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
+import { ThemeProvider } from '@material-ui/styles';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { CloudinaryContext } from 'cloudinary-react';
+import { SnackbarProvider } from 'notistack';
import configureStore, { history } from 'redux/configureStore';
import config from 'constants/config';
+import theme from './material-ui-theme';
const { store, persistor } = configureStore();
@@ -25,7 +28,17 @@ ReactDOM.render(
} persistor={persistor}>
-
+
+
+
+
+
,
diff --git a/frontend/src/material-ui-theme.js b/frontend/src/material-ui-theme.js
new file mode 100644
index 000000000..50efdc0e6
--- /dev/null
+++ b/frontend/src/material-ui-theme.js
@@ -0,0 +1,73 @@
+import { createMuiTheme } from '@material-ui/core/styles';
+
+const titleFont = ['"Montserrat"', 'sans-serif'].join(',');
+const bodyFont = ['"Lato"', 'sans-serif'].join(',');
+
+const theme = createMuiTheme({
+ palette: {
+ primary: {
+ main: '#52d7af'
+ },
+ secondary: {
+ main: '#f38100'
+ }
+ },
+ typography: {
+ fontFamily: bodyFont,
+ fontWeightRegular: 300,
+ h1: {
+ fontFamily: titleFont,
+ fontWeight: 'bold'
+ },
+ h2: {
+ fontFamily: titleFont,
+ fontWeight: 'bold'
+ },
+ h3: {
+ fontFamily: titleFont,
+ fontWeight: 'bold'
+ },
+ h4: {
+ fontFamily: titleFont,
+ fontWeight: 'bold'
+ },
+ h5: {
+ fontFamily: titleFont,
+ fontWeight: 'bold'
+ },
+ h6: {
+ fontFamily: titleFont,
+ fontWeight: 'bold'
+ },
+ subtitle1: {
+ fontFamily: bodyFont,
+ fontWeight: '300'
+ },
+ subtitle2: {
+ fontFamily: bodyFont,
+ fontWeight: '300'
+ },
+ body1: {
+ fontFamily: bodyFont,
+ fontWeight: '300'
+ },
+ body2: {
+ fontFamily: bodyFont,
+ fontWeight: '300'
+ },
+ button: {
+ fontFamily: bodyFont,
+ fontWeight: '300'
+ },
+ caption: {
+ fontFamily: bodyFont,
+ fontWeight: '300'
+ },
+ overline: {
+ fontFamily: bodyFont,
+ fontWeight: '300'
+ }
+ }
+});
+
+export default theme;
diff --git a/frontend/src/pages/EventDashboard/EventDashboardHome/EventDashboardHomeRegistration/TravelGrantStatusBlock.js b/frontend/src/pages/EventDashboard/EventDashboardHome/EventDashboardHomeRegistration/TravelGrantStatusBlock.js
index 2f3e67a62..781800fad 100644
--- a/frontend/src/pages/EventDashboard/EventDashboardHome/EventDashboardHomeRegistration/TravelGrantStatusBlock.js
+++ b/frontend/src/pages/EventDashboard/EventDashboardHome/EventDashboardHomeRegistration/TravelGrantStatusBlock.js
@@ -31,28 +31,59 @@ const TravelGrantStatusBlock = ({ event, registration }) => {
}
if (registration.status === STATUSES.confirmed.id) {
- return (
-
-
-
- Please consult the{' '}
-
- FAQ section
- {' '}
- of our website for details on the travel grant amounts available for the country you're
- travelling from.
-
- }
- />
-
- );
+ if (registration.travelGrant === 0) {
+ return (
+
+
+
+
+ );
+ }
+
+ if (!registration.travelGrant) {
+ return (
+
+
+
+ Please consult the{' '}
+
+ FAQ section
+ {' '}
+ of our website for details on the travel grant amounts available for the country you're
+ travelling from.
+
+ }
+ />
+
+ );
+ }
+
+ if (registration.travelGrant > 0) {
+ return (
+
+
+
+
+ );
+ }
}
+
return null;
};
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventInfo/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/BasicInfo/index.js
similarity index 99%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventInfo/index.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/BasicInfo/index.js
index 397548a23..f29cffdf0 100644
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventInfo/index.js
+++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/BasicInfo/index.js
@@ -6,7 +6,7 @@ import * as Validate from 'services/validation';
import Divider from 'components/generic/Divider';
import FormikImage from 'components/Forms/FormikImage';
import FormikField from 'components/FormComponents/FormikField';
-import MarkdownInput from 'components/FormComponents/MarkdownInput/index';
+import MarkdownInput from 'components/FormComponents/MarkdownInput';
const OrganiserEditEventInfo = props => {
return (
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/FiltersDrawer.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Details.module.scss
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/FiltersDrawer.module.scss
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Details.module.scss
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventMisc/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Miscellaneous/index.js
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventMisc/index.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Miscellaneous/index.js
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/AddQuestionModal/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/AddQuestionModal/index.js
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/AddQuestionModal/index.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/AddQuestionModal/index.js
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/AddSectionModal/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/AddSectionModal/index.js
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/AddSectionModal/index.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/AddSectionModal/index.js
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/CustomSectionList/CustomSectionList.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/CustomSectionList/CustomSectionList.module.scss
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/CustomSectionList/CustomSectionList.module.scss
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/CustomSectionList/CustomSectionList.module.scss
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/CustomSectionList/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/CustomSectionList/index.js
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/CustomSectionList/index.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/CustomSectionList/index.js
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/CustomSectionListItem/CustomSectionListItem.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/CustomSectionListItem/CustomSectionListItem.module.scss
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/CustomSectionListItem/CustomSectionListItem.module.scss
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/CustomSectionListItem/CustomSectionListItem.module.scss
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/CustomSectionListItem/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/CustomSectionListItem/index.js
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/CustomSectionListItem/index.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/CustomSectionListItem/index.js
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/index.js
similarity index 98%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/index.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/index.js
index fd3a635c4..40672b238 100644
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventRegistration/index.js
+++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Questions/index.js
@@ -14,7 +14,7 @@ const OrganiserEditEventRegistration = props => {
name="userDetailsConfig"
isFast={true}
render={({ field }) => {
- const fieldValue = field.value;
+ const fieldValue = field.value || {};
const fieldKeys = Object.keys(fieldValue);
const dataSource = fieldKeys.map(field => ({
key: field,
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventTimes/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Schedule/index.js
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventTimes/index.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Schedule/index.js
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventTimes/style.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Schedule/style.scss
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventTimes/style.scss
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/Schedule/style.scss
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/index.js
new file mode 100644
index 000000000..b56b77d88
--- /dev/null
+++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Details/index.js
@@ -0,0 +1,131 @@
+import React from 'react';
+import styles from './Details.module.scss';
+
+import { notification } from 'antd';
+import { Formik } from 'formik';
+import { connect } from 'react-redux';
+import { forOwn } from 'lodash-es';
+import * as OrganiserSelectors from 'redux/organiser/selectors';
+import * as OrganiserActions from 'redux/organiser/actions';
+import BlockExitIfDirty from 'components/FormComponents/BlockExitIfDirty';
+import PageHeader from 'components/generic/PageHeader';
+import PageWrapper from 'components/PageWrapper';
+import MaterialTabsLayout from 'components/layouts/MaterialTabsLayout';
+
+import { Box, Button } from '@material-ui/core';
+
+import BasicInfoTab from './BasicInfo';
+import ScheduleTab from './Schedule';
+import QuestionsTab from './Questions';
+import MiscellaneousTab from './Miscellaneous';
+
+const OrganiserEditEventDetails = ({ event, loading, editEvent }) => {
+ const { slug } = event;
+ function onSubmit(values, actions) {
+ const changed = {};
+ forOwn(values, (value, field) => {
+ if (event[field] !== value) {
+ changed[field] = value;
+ }
+ });
+ editEvent(slug, changed)
+ .then(savedEvent => {
+ notification.success({
+ message: 'Your changes were saved successfully'
+ });
+ actions.setSubmitting(false);
+ })
+ .catch(err => {
+ const { message, errors } = err.response.data;
+
+ if (errors) {
+ const errorKeys = Object.keys(errors);
+
+ notification.error({
+ message: 'Unable to save changes',
+ description: (
+
+ {errorKeys.map(key => (
+
+ {key} {errors[key].message}
+
+ ))}
+
+ )
+ });
+ } else {
+ notification.error({
+ message: 'Unable to save changes',
+ description: message
+ });
+ }
+ })
+ .finally(() => {
+ actions.setSubmitting(false);
+ });
+ }
+
+ return (
+
+
+
+ {formikProps => {
+ const errorCount = Object.keys(formikProps.errors).length;
+ const hasErrors = errorCount !== 0;
+ const canSave = formikProps.dirty && !hasErrors;
+ const isPublic = formikProps.values.published;
+
+ return (
+
+
+
+ Save changes
+
+
+
+ },
+ {
+ label: 'Schedule',
+ content:
+ },
+ {
+ label: 'Questions',
+ content:
+ },
+ {
+ label: 'Miscellaneous',
+ content:
+ }
+ ]}
+ />
+
+
+ );
+ }}
+
+
+ );
+};
+
+const mapStateToProps = state => ({
+ event: OrganiserSelectors.event(state),
+ loading: OrganiserSelectors.eventLoading(state)
+});
+
+const mapDispatchToProps = dispatch => ({
+ editEvent: (slug, data) => dispatch(OrganiserActions.editEvent(slug, data))
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(OrganiserEditEventDetails);
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventManage/AddOrganiserDrawer.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Manage/AddOrganiserDrawer.js
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventManage/AddOrganiserDrawer.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Manage/AddOrganiserDrawer.js
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventManage/AddOrganiserDrawer.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Manage/AddOrganiserDrawer.module.scss
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventManage/AddOrganiserDrawer.module.scss
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Manage/AddOrganiserDrawer.module.scss
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventManage/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Manage/index.js
similarity index 50%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventManage/index.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Manage/index.js
index 81934eaf8..01900b71e 100644
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventManage/index.js
+++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Manage/index.js
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import { PageHeader, Button, Table, notification, message } from 'antd';
+import { Button, Table, notification, message } from 'antd';
import { concat } from 'lodash-es';
import { connect } from 'react-redux';
import { push } from 'connected-react-router';
@@ -8,7 +8,7 @@ import * as OrganiserActions from 'redux/organiser/actions';
import * as OrganiserSelectors from 'redux/organiser/selectors';
import AddOrganiserDrawer from './AddOrganiserDrawer';
-import Divider from 'components/generic/Divider';
+import PageHeader from 'components/generic/PageHeader';
import PageWrapper from 'components/PageWrapper';
const OrganiserEditEventManage = ({
@@ -68,71 +68,54 @@ const OrganiserEditEventManage = ({
});
}
- const testEmail = () => {
- const recipient = 'juuso.lappalainen@hackjunction.com';
- };
-
return (
(
- Manage who has access to edit this event}
- extra={[
- setDrawerOpen(true)}>
- Add organisers
-
+
+ setDrawerOpen(true)}>
+ Add organisers
+
+ record.firstName + ' ' + record.lastName
+ },
+ {
+ title: 'Email',
+ dataIndex: 'email',
+ key: 'email'
+ },
+ {
+ title: 'Actions',
+ dataIndex: 'userId',
+ key: 'actions',
+ render: (text, record) => {
+ if (event.owner === record.userId) {
+ return (
+
+ Owner
+
+ );
+ }
+ return (
+ handleOrganiserRemoved(record.userId)}>
+ Remove
+
+ );
+ }
+ }
]}
- footer={
-
- record.firstName + ' ' + record.lastName
- },
- {
- title: 'Email',
- dataIndex: 'email',
- key: 'email'
- },
- {
- title: 'Actions',
- dataIndex: 'userId',
- key: 'actions',
- render: (text, record) => {
- if (event.owner === record.userId) {
- return (
-
- Owner
-
- );
- }
- return (
- handleOrganiserRemoved(record.userId)}
- >
- Remove
-
- );
- }
- }
- ]}
- />
-
-
- }
/>
-
-
- Test sending email to juuso.lappalainen@hackjunction.com
-
)}
/>
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeeDrawer.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeeDrawer.js
deleted file mode 100644
index 9c283d5e2..000000000
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeeDrawer.js
+++ /dev/null
@@ -1,196 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import styles from './AttendeeDrawer.module.scss';
-
-import { connect } from 'react-redux';
-import { RegistrationFields, RegistrationStatuses } from '@hackjunction/shared';
-import { Drawer, Skeleton, Descriptions, Tabs, Button, Tag, Popconfirm } from 'antd';
-import { find, groupBy } from 'lodash-es';
-
-import * as AuthSelectors from 'redux/auth/selectors';
-import * as OrganiserActions from 'redux/organiser/actions';
-import * as OrganiserSelectors from 'redux/organiser/selectors';
-import Divider from 'components/generic/Divider';
-import AttendeeDrawerEdit from './AttendeeDrawerEdit';
-
-const AttendeeDrawer = ({
- event,
- idToken,
- slug,
- registrationId,
- isOpen,
- onClose,
- editAttendee,
- acceptAttendee,
- rejectAttendee,
- updateAttendee,
- getAttendee
-}) => {
- const [editOpen, setEditOpen] = useState(false);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(false);
- const registration = getAttendee(slug, registrationId);
- const { answers } = registration;
-
- useEffect(() => {
- if (registrationId) {
- setLoading(true);
- updateAttendee(slug, registrationId)
- .catch(err => {
- setError(true);
- })
- .finally(() => {
- setLoading(false);
- });
- }
- }, [registrationId, slug, updateAttendee]);
-
- function handleRegistrationEdit(values) {
- editAttendee(slug, registrationId, values);
- }
-
- function handleAccept() {
- acceptAttendee(slug, registrationId);
- }
-
- function handleReject() {
- rejectAttendee(slug, registrationId);
- }
-
- function renderError() {
- return (
-
-
Something went wrong
-
Please close the drawer and try again
-
- );
- }
-
- function renderContent() {
- const fields = Object.keys(answers);
- const grouped = groupBy(fields, field => RegistrationFields.getCategory(field));
- const categoryNames = Object.keys(grouped);
- const currentStatus = RegistrationStatuses.asObject[registration.status];
-
- return (
-
- {currentStatus.allowEdit && (
-
-
Actions:
-
-
-
- Accept
-
-
-
-
-
- Reject
-
-
-
- )}
-
- setEditOpen(true)}>
- Edit
-
- }
- >
- {categoryNames.map((categoryName, index) => {
- return (
-
-
- {grouped[categoryName].map(field => {
- let label = RegistrationFields.fieldToLabelMap[field];
- if (!label) {
- const customField = find(
- event.registrationQuestions,
- f => f.name === field
- );
- if (customField) {
- label = customField.label;
- }
- }
- return (
-
- {JSON.stringify(registration.answers[field])}
-
- );
- })}
-
-
- );
- })}
-
- setEditOpen(false)}
- onSubmit={handleRegistrationEdit}
- registration={registration}
- />
-
- );
- }
-
- function renderTitle() {
- const currentStatus = RegistrationStatuses.asObject[registration.status];
- return (
-
-
{registration.answers.firstName + ' ' + registration.answers.lastName}
-
-
status: {currentStatus.label}
-
- );
- }
- return (
-
-
- {error && renderError()}
- {!error && answers && renderContent()}
-
-
- );
-};
-
-const mapStateToProps = state => ({
- idToken: AuthSelectors.getIdToken(state),
- getAttendee: OrganiserSelectors.getAttendeeByEvent(state)
-});
-
-const mapDispatchToProps = dispatch => ({
- editAttendee: (slug, registrationId, data) => dispatch(OrganiserActions.editAttendee(slug, registrationId, data)),
- acceptAttendee: (slug, registrationId) => dispatch(OrganiserActions.acceptAttendee(slug, registrationId)),
- rejectAttendee: (slug, registrationId) => dispatch(OrganiserActions.rejectAttendee(slug, registrationId)),
- updateAttendee: (slug, registrationId) => dispatch(OrganiserActions.updateAttendee(slug, registrationId))
-});
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(AttendeeDrawer);
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeeDrawer.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeeDrawer.module.scss
deleted file mode 100644
index c2dc51e53..000000000
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeeDrawer.module.scss
+++ /dev/null
@@ -1,25 +0,0 @@
-.drawerTitle {
- display: flex;
- flex-direction: row;
-}
-
-.buttonsWrapper {
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
- background: #ececec;
- padding: 1rem;
-}
-
-.acceptButton {
- color: green;
-}
-
-.rejectButton {
- color: red;
-}
-
-.buttonsLabel {
- font-weight: bold;
-}
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeeDrawerEdit.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeeDrawerEdit.js
deleted file mode 100644
index dc347fcbe..000000000
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeeDrawerEdit.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import React from 'react';
-import styles from './AttendeeDrawerEdit.module.scss';
-
-import { filter } from 'lodash-es';
-import { connect } from 'react-redux';
-import { RegistrationStatuses } from '@hackjunction/shared';
-import { Formik } from 'formik';
-import { Drawer, Descriptions, Button, Rate, Tag, Dropdown, Menu, Icon } from 'antd';
-import Divider from 'components/generic/Divider';
-import * as AuthSelectors from 'redux/auth/selectors';
-
-const AttendeeDrawerEdit = ({ isOpen, onClose, onSubmit, registration, session }) => {
- function buildInitial() {
- return {
- status: registration.status,
- rating: registration.rating,
- ratedBy: registration.ratedBy
- };
- }
-
- function handleSubmit(values) {
- onSubmit(values);
- onClose();
- }
-
- function handleRatingChange(rating, setFieldValue) {
- setFieldValue('rating', rating);
- setFieldValue('ratedBy', session.sub);
- }
-
- function renderStatusMenu(currentStatus, setFieldValue) {
- const statuses = RegistrationStatuses.asArray.filter(status => status.id !== currentStatus.id);
- const assignable = filter(statuses, s => s.allowAssign);
- return (
-
- {assignable.map(status => (
- setFieldValue('status', status.id)}>
- {status.label}
-
- ))}
-
- );
- }
-
- const initialValues = buildInitial();
-
- return (
-
-
- {formikProps => {
- const { values } = formikProps;
- const currentStatus = RegistrationStatuses.asObject[values.status];
- const initialStatus = RegistrationStatuses.asObject[initialValues.status];
- return (
-
-
-
-
-
- {currentStatus.label}
- }
- type="link"
- >
- Change
-
-
-
-
{currentStatus.description}
-
-
-
- handleRatingChange(value, formikProps.setFieldValue)}
- />
-
-
- {values.ratedBy}
-
-
-
-
- Save changes
-
-
- );
- }}
-
-
- );
-};
-
-const mapStateToProps = state => ({
- session: AuthSelectors.getCurrentUser(state)
-});
-
-export default connect(mapStateToProps)(AttendeeDrawerEdit);
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeeDrawerEdit.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeeDrawerEdit.module.scss
deleted file mode 100644
index 9b1df2879..000000000
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeeDrawerEdit.module.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-.statusWrapper {
- display: flex;
- align-items: flex-start;
- flex-direction: column;
-}
-
-.menuSubHeader {
- text-align: center;
- color: red;
- padding: 10px;
- width: 200px;
-}
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeesOverview.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeesOverview.js
deleted file mode 100644
index e891b0a9a..000000000
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/AttendeesOverview.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import React, { useEffect } from 'react';
-import styles from './AttendeesOverview.module.scss';
-
-import { Row, Col, Card, Statistic, Icon } from 'antd';
-import RegistrationsService from 'services/registrations';
-
-const STATS = [
- {
- label: 'Applications',
- getValue: () => 272
- },
- {
- label: 'Amount rated',
- getValue: () => 16,
- suffix: '%'
- }
-];
-
-const AttendeesOverview = () => {
- const renderStatCards = () => {
- return STATS.map(stat => {
- return (
-
-
-
-
-
- );
- });
- };
-
- return {renderStatCards()}
;
-};
-
-export default AttendeesOverview;
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/FiltersDrawer.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/FiltersDrawer.js
deleted file mode 100644
index f7ffdeaf8..000000000
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/FiltersDrawer.js
+++ /dev/null
@@ -1,153 +0,0 @@
-import React from 'react';
-import styles from './FiltersDrawer.module.scss';
-import { connect } from 'react-redux';
-import { concat, filter, map } from 'lodash-es';
-import { RegistrationStatuses, RegistrationFields } from '@hackjunction/shared';
-import { Drawer, Tag, Descriptions, Input, Button, Select, Row, Col } from 'antd';
-import { Formik } from 'formik';
-
-import Divider from 'components/generic/Divider';
-import * as OrganiserSelectors from 'redux/organiser/selectors';
-import * as OrganiserActions from 'redux/organiser/actions';
-
-const FiltersDrawer = ({ onClose, isOpen, getFilters, setFilters, event }) => {
- const filters = getFilters(event.slug);
-
- function handleSubmit(values) {
- setFilters(event.slug, values);
- onClose();
- }
-
- function handleReset() {
- setFilters(event.slug, {});
- onClose();
- }
-
- function renderStatusSelect(formikProps) {
- const selected = formikProps.values.status || [];
- return RegistrationStatuses.asArray.map(status => {
- const checked = selected.indexOf(status.id) !== -1;
- return (
- {
- if (checked) {
- formikProps.setFieldValue('status', concat(selected, status.id));
- } else {
- formikProps.setFieldValue('status', filter(selected, s => s !== status.id));
- }
- }}
- >
- {status.label}
-
- );
- });
- }
-
- function renderSearchFilter(formikProps) {
- const currentValue = formikProps.values.email || '';
- return (
- formikProps.setFieldValue('email', e.target.value)}
- />
- );
- }
-
- function renderFieldExists(formikProps) {
- const currentValue = formikProps.values.fields || undefined;
- const userDetailFields = Object.keys(event.userDetailsConfig).map(q => ({
- name: q,
- label: RegistrationFields.getLabel(q)
- }));
- const registrationFields = map(event.registrationQuestions, q => ({
- name: q.name,
- label: q.label
- }));
- return (
- formikProps.setFieldValue('fields', value)}
- value={currentValue}
- style={{ width: '100%' }}
- mode="multiple"
- placeholder={'Select fields'}
- >
-
- {userDetailFields.map(field => (
- {field.label}
- ))}
-
-
- {registrationFields.map(field => (
- {field.label}
- ))}
-
-
- );
- }
-
- return (
-
-
- {formikProps => {
- return (
-
-
-
- Filter participants by application status
- {renderStatusSelect(formikProps)}
-
-
- Search by email address
- {renderSearchFilter(formikProps)}
-
-
- Select all fields that must exist on the application
- {renderFieldExists(formikProps)}
-
-
-
-
-
-
- Clear filters
-
-
-
-
- Save filters
-
-
-
-
- );
- }}
-
-
- );
-};
-
-const mapStateToProps = state => ({
- getFilters: OrganiserSelectors.getAttendeesFiltersForEvent(state)
-});
-
-const mapDispatchToProps = dispatch => ({
- setFilters: (slug, filters) => dispatch(OrganiserActions.setAttendeesFiltersForEvent(slug, filters))
-});
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(FiltersDrawer);
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/index.js
deleted file mode 100644
index 9e134acac..000000000
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/index.js
+++ /dev/null
@@ -1,186 +0,0 @@
-import React, { useEffect, useState, useCallback } from 'react';
-
-import { PageHeader, Table, Dropdown, Menu, Icon, Tag, Button, Menu } from 'antd';
-import { connect } from 'react-redux';
-import { isEmpty } from 'lodash-es';
-import { RegistrationStatuses } from '@hackjunction/shared';
-
-import MiscUtils from 'utils/misc';
-import * as AuthSelectors from 'redux/auth/selectors';
-import * as OrganiserSelectors from 'redux/organiser/selectors';
-import * as OrganiserActions from 'redux/organiser/actions';
-import Divider from 'components/generic/Divider';
-import AttendeeDrawer from './AttendeeDrawer';
-import FiltersDrawer from 'pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventAttendees/FiltersDrawer';
-
-const OrganiserEditEventAttendees = ({
- slug,
- idToken,
- getEventBySlug,
- updateAttendees,
- getAttendeeByEvent,
- getAttendeeIds,
- getAttendeeFilters
-}) => {
- const [activeItem, setActiveItem] = useState();
- const [filtersOpen, setFiltersOpen] = useState(false);
- const [selectedRowKeys, setSelectedRowKeys] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(false);
- const event = getEventBySlug(slug);
- const attendeeIds = getAttendeeIds(slug);
- const filters = getAttendeeFilters(slug);
- const hasFilters = !isEmpty(filters);
-
- const updateAttendeesCallback = useCallback(() => {
- updateAttendees(slug)
- .catch(() => {
- setError(true);
- })
- .finally(() => {
- setLoading(false);
- });
- }, [slug, updateAttendees]);
-
- useEffect(() => {
- setLoading(true);
- updateAttendeesCallback();
- }, [updateAttendeesCallback]);
-
- async function handleFiltersClosed() {
- setFiltersOpen(false);
- setLoading(true);
- await MiscUtils.sleep(1000);
- updateAttendees(slug)
- .catch(() => {
- setError(true);
- })
- .finally(() => {
- setLoading(false);
- });
- }
-
- function renderTop() {
- return View who has registered to your event
;
- }
-
- function renderContent() {
- return (
-
-
{
- const { color, label } = RegistrationStatuses.asObject[status];
- return {label} ;
- }
- },
- {
- title: 'Actions',
- dataIndex: 'email',
- render: (email, record) => {
- return (
- (
-
- setActiveItem(record._id)}>Show
- Full Details
- Show Profile
- Delete
-
- )}
- >
-
- Actions
-
-
- );
- }
- }
- ]}
- dataSource={attendeeIds.map(id => getAttendeeByEvent(slug, id))}
- pagination={false}
- footer={() => {selectedRowKeys.length} selected }
- />
-
- );
- }
-
- return (
-
-
- Search
- Assigned to me
- 3rd section
-
-
- Filters active
-
- ),
- setFiltersOpen(true)}>
- Filters
-
- ]}
- footer={
-
-
- {!error && renderContent()}
- setActiveItem(undefined)}
- />
-
-
-
- }
- />
-
- );
-};
-
-const mapStateToProps = state => ({
- idToken: AuthSelectors.getIdToken(state),
- getEventBySlug: OrganiserSelectors.getEventBySlug(state),
- getAttendeeByEvent: OrganiserSelectors.getAttendeeByEvent(state),
- getAttendeeIds: OrganiserSelectors.getAttendeeIdsForEvent(state),
- getAttendeeFilters: OrganiserSelectors.getAttendeesFiltersForEvent(state)
-});
-
-const mapDispatchToProps = dispatch => ({
- updateAttendees: slug => dispatch(OrganiserActions.updateAttendeesForEvent(slug))
-});
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(OrganiserEditEventAttendees);
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDebug/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDebug/index.js
deleted file mode 100644
index 6e1926bb6..000000000
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDebug/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-import ReactJson from 'react-json-view';
-
-import { connect } from 'react-redux';
-import * as OrganiserSelectors from 'redux/organiser/selectors';
-
-const OrganiserEditEventDebug = ({ slug, getEventBySlug }) => {
- const event = getEventBySlug(slug);
-
- return ;
-};
-
-const mapStateToProps = state => ({
- getEventBySlug: OrganiserSelectors.getEventBySlug(state)
-});
-
-export default connect(mapStateToProps)(OrganiserEditEventDebug);
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventDetails.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/OrganiserEditEventDetails.module.scss
deleted file mode 100644
index e69de29bb..000000000
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/index.js
deleted file mode 100644
index 84b1f7f3f..000000000
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventDetails/index.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import React from 'react';
-import styles from './OrganiserEditEventDetails.module.scss';
-
-import { Tabs, PageHeader, Button, notification } from 'antd';
-import { Formik } from 'formik';
-import { connect } from 'react-redux';
-import { forOwn } from 'lodash-es';
-import * as OrganiserSelectors from 'redux/organiser/selectors';
-import * as OrganiserActions from 'redux/organiser/actions';
-import Divider from 'components/generic/Divider';
-import OrganiserEditEventInfo from './OrganiserEditEventInfo';
-import OrganiserEditEventTimes from './OrganiserEditEventTimes';
-import OrganiserEditEventRegistration from './OrganiserEditEventRegistration';
-import OrganiserEditEventMisc from './OrganiserEditEventMisc';
-import BlockExitIfDirty from 'components/FormComponents/BlockExitIfDirty';
-
-const { TabPane } = Tabs;
-
-const OrganiserEditEventDetails = ({ event, editEvent }) => {
- const { slug } = event;
- function onSubmit(values, actions) {
- const changed = {};
- forOwn(values, (value, field) => {
- if (event[field] !== value) {
- changed[field] = value;
- }
- });
- editEvent(slug, changed)
- .then(savedEvent => {
- notification.success({
- message: 'Your changes were saved successfully'
- });
- actions.setSubmitting(false);
- })
- .catch(err => {
- const { message, errors } = err.response.data;
-
- if (errors) {
- const errorKeys = Object.keys(errors);
-
- notification.error({
- message: 'Unable to save changes',
- description: (
-
- {errorKeys.map(key => (
-
- {key} {errors[key].message}
-
- ))}
-
- )
- });
- } else {
- notification.error({
- message: 'Unable to save changes',
- description: message
- });
- }
- })
- .finally(() => {
- actions.setSubmitting(false);
- });
- }
-
- return (
-
- {formikProps => {
- const errorCount = Object.keys(formikProps.errors).length;
- const hasErrors = errorCount !== 0;
- const canSave = formikProps.dirty && !hasErrors;
- const isPublic = formikProps.values.published;
-
- return (
-
- Configure your event information, registration settings and schedule}
- extra={[
- {
- formikProps.setFieldValue('published', !isPublic);
- }}
- >
- {isPublic ? 'Un-publish' : 'Publish'}
- ,
- formikProps.submitForm()}
- key="1"
- >
- Save
-
- ]}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }}
-
- );
-};
-
-const mapStateToProps = state => ({
- event: OrganiserSelectors.event(state)
-});
-
-const mapDispatchToProps = dispatch => ({
- editEvent: (slug, data) => dispatch(OrganiserActions.editEvent(slug, data))
-});
-
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(OrganiserEditEventDetails);
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AttendeeFilters.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AttendeeFilters.js
deleted file mode 100644
index 12c3aee23..000000000
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AttendeeFilters.js
+++ /dev/null
@@ -1,343 +0,0 @@
-import React, { useState, useCallback, useEffect, useMemo } from 'react';
-import './AttendeeFilters.scss';
-
-import { connect } from 'react-redux';
-import { Button as AntButton, Select, Row, Col, Rate, Tag, Collapse, Input, Steps } from 'antd';
-import { RegistrationStatuses, RegistrationFields } from '@hackjunction/shared';
-import { find } from 'lodash-es';
-
-import Divider from 'components/generic/Divider';
-import * as OrganiserSelectors from 'redux/organiser/selectors';
-import * as OrganiserActions from 'redux/organiser/actions';
-import FilterOptions from 'constants/filters';
-
-const AttendeeFilters = ({ event, registrations, filters = [], setFilters }) => {
- const [filterType, setFilterType] = useState();
- const [filterValue, setFilterValue] = useState();
- const [filterField, setFilterField] = useState();
- const disabled = !filterType;
-
- useEffect(() => {
- setFilterType(undefined);
- setFilterValue(undefined);
- setFilterField(undefined);
- }, [filters]);
-
- const handleAdd = useCallback(() => {
- const filter = {
- type: filterType,
- value: filterValue,
- field: filterField
- };
- setFilters(filters.concat(filter));
- }, [filterType, filterValue, filterField, filters, setFilters]);
-
- const handleRemove = useCallback(
- index => {
- const newFilters = filters.filter((f, idx) => {
- return idx !== index;
- });
- setFilters(newFilters);
- },
- [filters, setFilters]
- );
-
- const handleReset = useCallback(() => {
- setFilters([]);
- }, [setFilters]);
-
- const handleTypeChange = useCallback(type => {
- setFilterType(type);
- setFilterValue(undefined);
- }, []);
-
- const handleFieldChange = useCallback(field => {
- setFilterField(field);
- }, []);
-
- const questionSelect = useMemo(() => {
- return (
-
-
- {event.userDetailsConfig &&
- Object.keys(event.userDetailsConfig).map(fieldName => {
- return (
-
- {RegistrationFields.getLabel(fieldName)}
-
- );
- })}
-
- {event.customQuestions &&
- event.customQuestions.map(section => {
- return (
-
-
- {section.label} (any)
-
- {section.questions.map(question => (
-
- {section.label}: {question.label}
-
- ))}
-
- );
- })}
-
- );
- }, [event, handleFieldChange, filterField]);
-
- const renderOptions = () => {
- switch (filterType) {
- case 'status-equals':
- case 'status-nequals': {
- return (
-
- {RegistrationStatuses.asArray.map(status => (
-
- {status.label}
-
- ))}
-
- );
- }
- case 'rating-lte':
- case 'rating-gte':
- return ;
- case 'tags-contain':
- case 'tags-not-contain':
- return (
-
- {event.tags &&
- event.tags.map(tag => (
-
- {tag.label}
-
- ))}
-
- );
- case 'field-equals':
- case 'field-nequals':
- case 'field-contains':
- case 'field-not-contains':
- return (
-
- {questionSelect}
-
- setFilterValue(e.target.value)}
- placeholder="Enter value"
- size="large"
- />
-
- );
- case 'field-not-empty':
- case 'field-empty':
- return questionSelect;
- default:
- return null;
- }
- };
-
- const renderForm = () => {
- return (
-
-
-
-
- {FilterOptions.map(({ id, label }) => (
-
- {label}
-
- ))}
-
-
-
-
- {renderOptions()}
-
-
-
-
- Add filter
-
-
-
-
-
-
- );
- };
-
- const renderItemValue = (filter, label) => {
- switch (filter.type) {
- case 'status-equals':
- case 'status-nequals': {
- const statuses = RegistrationStatuses.asArray
- .filter(status => {
- return filter.value && filter.value.indexOf(status.id) !== -1;
- })
- .map(status => {
- return {status.label} ;
- });
- return (
-
- {label} {statuses}
-
- );
- }
- case 'rating-lte':
- case 'rating-gte': {
- return (
-
- {label}
-
- );
- }
- case 'tags-contain':
- case 'tags-not-contain':
- const tags = event.tags
- .filter(tag => {
- return filter.value && filter.value.indexOf(tag.label) !== -1;
- })
- .map(tag => {
- return {tag.label} ;
- });
- return (
-
- {label} {tags}
-
- );
- case 'field-equals':
- return (
-
- {filter.field} EQUALS {filter.value}
-
- );
- case 'field-nequals':
- return (
-
- {filter.field} DOES NOT EQUAL {filter.value}
-
- );
- case 'field-empty':
- return (
-
- {filter.field} IS EMPTY
-
- );
- case 'field-not-empty':
- return (
-
- {filter.field} IS NOT EMPTY
-
- );
- case 'field-contains': {
- return (
-
- {filter.field} CONTAINS {filter.value}
-
- );
- }
- case 'field-not-contains': {
- return (
-
- {filter.field} DOES NOT CONTAIN {filter.value}
-
- );
- }
- default:
- return {label} ;
- }
- };
-
- const renderFilterSteps = () => {
- return filters.map((filter, idx) => {
- const label = find(FilterOptions, option => option.id === filter.type).label;
-
- return (
- {renderItemValue(filter, label)}}
- description={
- handleRemove(idx)}>
- Remove filter
-
- }
- />
- );
- });
- };
-
- return (
-
-
- Clear filters
-
- }
- >
- {renderForm()}
-
- index !== 0 && AND
- }
- direction="vertical"
- className="AttendeeFilters--steps"
- >
- {renderFilterSteps()}
-
-
-
- );
-};
-
-const mapState = state => ({
- event: OrganiserSelectors.event(state),
- registrations: OrganiserSelectors.registrations(state),
- filters: OrganiserSelectors.registrationsFilters(state)
-});
-
-const mapDispatch = dispatch => ({
- setFilters: filters => dispatch(OrganiserActions.setRegistrationsFilters(filters))
-});
-
-export default connect(
- mapState,
- mapDispatch
-)(AttendeeFilters);
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AttendeeFilters.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AttendeeFilters.scss
deleted file mode 100644
index 5cbe8ad3d..000000000
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AttendeeFilters.scss
+++ /dev/null
@@ -1,30 +0,0 @@
-.AttendeeFilters--steps {
- &-top {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- }
-
- .ant-steps-item {
- border-bottom: 1px solid rgba(0, 0, 0, 0.1);
- margin-bottom: 1rem;
- }
-
- .ant-steps-item .ant-steps-item-title {
- width: 100%;
- }
-
- .ant-steps-item .ant-steps-item-content {
- width: auto;
- padding-top: 4px;
- margin-left: 50px;
- }
-
- .ant-steps-item .ant-steps-item-tail {
- display: none !important;
- }
-
- &-icon {
- display: block;
- }
-}
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/SearchAttendeesPage.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/SearchAttendeesPage.js
deleted file mode 100644
index dfce04240..000000000
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/SearchAttendeesPage.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react';
-import styles from './SearchAttendeesPage.module.scss';
-import { connect } from 'react-redux';
-import { Button as AntButton } from 'antd';
-
-import * as OrganiserSelectors from 'redux/organiser/selectors';
-import * as FilterUtils from 'utils/filters';
-
-import Divider from 'components/generic/Divider';
-import AttendeeTable from 'components/tables/AttendeeTable';
-import BulkEditRegistrationDrawer from 'components/modals/BulkEditRegistrationDrawer';
-import BulkEmailDrawer from 'components/modals/BulkEmailDrawer';
-import AttendeeFilters from './AttendeeFilters';
-
-const SearchAttendeesPage = ({ registrations, registrationsLoading, filters }) => {
- const filtered = FilterUtils.applyFilters(registrations, filters);
-
- const renderBulkActions = () => {
- if (!registrations.length) return null;
- const ids = registrations.map(r => r._id);
- const userIds = registrations.map(r => r.user);
- return (
-
-
- {registrations.length} registrations
-
-
-
-
- );
- };
-
- return (
-
-
-
- {renderBulkActions()}
-
-
- );
-};
-
-const mapState = state => ({
- registrations: OrganiserSelectors.registrationsFiltered(state),
- registrationsLoading: OrganiserSelectors.registrationsLoading(state),
- filters: OrganiserSelectors.registrationsFilters(state)
-});
-
-export default connect(mapState)(SearchAttendeesPage);
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/index.js
deleted file mode 100644
index af122db33..000000000
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/index.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import React, { useState, useEffect, useCallback } from 'react';
-
-import { PageHeader, Menu, Button as AntButton } from 'antd';
-import { connect } from 'react-redux';
-
-import PageWrapper from 'components/PageWrapper';
-import Divider from 'components/generic/Divider';
-
-import * as OrganiserSelectors from 'redux/organiser/selectors';
-import * as OrganiserActions from 'redux/organiser/actions';
-
-import SearchAttendeesPage from './SearchAttendeesPage';
-import AssignAttendeesPage from './AssignAttendeesPage';
-import TeamsPage from './TeamsPage';
-import AdminPage from './AdminPage';
-
-const OrganiserEditEventReview = ({ event, organisers, updateRegistrations, updateTeams, registrationsLoading }) => {
- const [selectedKey, setSelectedKey] = useState('search');
-
- const updateData = useCallback(() => {
- updateRegistrations(event.slug);
- updateTeams(event.slug);
- }, [event.slug, updateTeams, updateRegistrations]);
-
- useEffect(() => {
- updateData();
- }, [event.slug, updateData]);
-
- const renderSelectedKey = () => {
- switch (selectedKey) {
- case 'search':
- return ;
- case 'teams':
- return ;
- case 'assigned':
- return ;
- case 'admin':
- return ;
- default:
- return null;
- }
- };
-
- return (
-
- Applications to your event}
- extra={
-
- Refresh data
-
- }
- footer={
-
- setSelectedKey(key)}
- >
- Participants
- Teams
- Assigned to you
- Admin & Tools
-
-
- {renderSelectedKey()}
-
- }
- />
-
- );
-};
-
-const mapState = state => ({
- organisers: OrganiserSelectors.organisers(state),
- event: OrganiserSelectors.event(state),
- registrationsLoading: OrganiserSelectors.registrationsLoading(state)
-});
-
-const mapDispatch = dispatch => ({
- updateRegistrations: slug => dispatch(OrganiserActions.updateRegistrationsForEvent(slug)),
- updateTeams: slug => dispatch(OrganiserActions.updateTeamsForEvent(slug))
-});
-
-export default connect(
- mapState,
- mapDispatch
-)(OrganiserEditEventReview);
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AdminPage.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Admin/AdminPage.scss
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AdminPage.scss
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Admin/AdminPage.scss
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AdminPage.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Admin/index.js
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AdminPage.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Admin/index.js
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AssignAttendeesPage.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Assigned/AssignAttendeesPage.module.scss
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AssignAttendeesPage.module.scss
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Assigned/AssignAttendeesPage.module.scss
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AssignAttendeesPage.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Assigned/index.js
similarity index 97%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AssignAttendeesPage.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Assigned/index.js
index a4b30e9b7..2e010f8f6 100644
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/AssignAttendeesPage.js
+++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Assigned/index.js
@@ -65,7 +65,7 @@ const SearchAttendeesPage = ({ idToken, event, registrations = [], registrations
Assign random registrations
- r._id)} />
+ {/* r._id)} /> */}
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/SearchAttendeesPage.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Search/SearchAttendeesPage.module.scss
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/SearchAttendeesPage.module.scss
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Search/SearchAttendeesPage.module.scss
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Search/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Search/index.js
new file mode 100644
index 000000000..ebc6d3005
--- /dev/null
+++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Search/index.js
@@ -0,0 +1,32 @@
+import React, { useState, useCallback, useEffect } from 'react';
+import styles from './SearchAttendeesPage.module.scss';
+import { connect } from 'react-redux';
+import { FilterHelpers } from '@hackjunction/shared';
+
+import * as OrganiserSelectors from 'redux/organiser/selectors';
+
+import Divider from 'components/generic/Divider';
+import AttendeeTable from 'components/tables/AttendeeTable';
+import FilterGroupMenu from 'components/filters/FilterGroupMenu';
+
+const SearchAttendeesPage = ({ registrations, registrationsLoading }) => {
+ const [filters, setFilters] = useState([]);
+ const filtered = FilterHelpers.applyFilters(registrations, filters);
+
+ return (
+
+
+
+ {/* {renderBulkActions()} */}
+
+
+ );
+};
+
+const mapState = state => ({
+ registrations: OrganiserSelectors.registrations(state),
+ registrationsLoading: OrganiserSelectors.registrationsLoading(state),
+ filters: []
+});
+
+export default connect(mapState)(SearchAttendeesPage);
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/TeamsPage.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Teams/TeamsPage.module.scss
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/TeamsPage.module.scss
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Teams/TeamsPage.module.scss
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/TeamsPage.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Teams/index.js
similarity index 97%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/TeamsPage.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Teams/index.js
index e5e9e8e62..2ad347a3c 100644
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventReview/TeamsPage.js
+++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Teams/index.js
@@ -26,10 +26,10 @@ const TeamsPage = ({ event, teams, registrationsLoading, teamsLoading, registrat
if (!team.members || !team.members.length) return null;
return (
- r._id)}
buttonProps={{ text: 'Edit all team members' }}
- />
+ /> */}
);
}}
@@ -106,10 +106,10 @@ const TeamsPage = ({ event, teams, registrationsLoading, teamsLoading, registrat
{teamsFiltered.length} teams
-
+ /> */}
{
+ const [calculations, setCalculations] = useState();
+ const [maxSpend, setMaxSpend] = useState(0);
+ const { slug } = event;
+
+ const makeCalculations = useCallback(() => {
+ const groupsSorted = sortBy(filterGroups, group => amountsByGroup[group.label] * -1);
+ const registrationsMapped = groupsSorted.reduce(
+ ({ registrations, res }, group) => {
+ const filtered = FilterHelpers.applyFilters(registrations, group.filters);
+ const amount = parseInt(amountsByGroup[group.label]);
+
+ if (amount === 0) {
+ return { registrations, res };
+ }
+
+ return {
+ res: res.concat(
+ filtered.map(reg => ({
+ id: reg._id,
+ group: group.label,
+ amount,
+ createdAt: reg.createdAt
+ }))
+ ),
+ registrations: difference(registrations, filtered)
+ };
+ },
+ {
+ registrations: eligibleRegistrations,
+ res: []
+ }
+ ).res;
+ const registrationsSorted = sortBy(registrationsMapped, 'createdAt');
+
+ const registrationsGranted = [];
+ let currentSpend = 0;
+ for (let item of registrationsSorted) {
+ if (currentSpend + item.amount <= maxSpend) {
+ currentSpend += item.amount;
+ registrationsGranted.push(item);
+ }
+ }
+
+ const byGroup = filterGroups.map(group => {
+ const amount = amountsByGroup[group.label];
+ const granted = registrationsGranted.filter(r => r.group === group.label);
+ const total = registrationsSorted.filter(r => r.group === group.label);
+
+ return {
+ group: group.label,
+ amount,
+ registrationsGranted: granted,
+ registrationsTotal: total,
+ totalSpend: sumBy(granted, 'amount')
+ };
+ });
+
+ const byGroupSorted = sortBy(byGroup, 'totalSpend');
+
+ setCalculations({
+ granted: registrationsGranted,
+ total: registrationsSorted,
+ spend: currentSpend,
+ byGroup: byGroupSorted
+ });
+ }, [filterGroups, eligibleRegistrations, maxSpend, amountsByGroup]);
+
+ const handleSubmit = useCallback(() => {
+ const data = calculations.granted.map(item => ({
+ _id: item.id,
+ amount: item.amount
+ }));
+
+ RegistrationsService.bulkAssignTravelGrantsForEvent(idToken, slug, data)
+ .then(() => {
+ enqueueSnackbar('Success!', { variant: 'success' });
+ })
+ .catch(err => {
+ enqueueSnackbar('Something went wrong...', { variant: 'error' });
+ console.log(err);
+ });
+ }, [idToken, slug, calculations, enqueueSnackbar]);
+
+ const canSubmit = calculations && calculations.granted.length > 0;
+
+ return (
+
+
+
+
+
+
+
+ Calculate
+
+
+
+
+ {calculations && (
+
+
+
+ This would result in a total spend of {calculations.spend} and give travel grants to{' '}
+ {calculations.granted.length} participants
+
+
+
+ `${granted.length}/${registrationsTotal.length}`
+ },
+ {
+ key: 'spend',
+ label: 'Spend (EUR)',
+ path: 'totalSpend'
+ }
+ ]}
+ />
+
+ )}
+
+
+
+
+ Assign travel grants
+
+
+
+
+ );
+};
+
+const mapState = state => ({
+ idToken: AuthSelectors.getIdToken(state),
+ event: OrganiserSelectors.event(state),
+ filterGroups: OrganiserSelectors.filterGroups(state),
+ eligibleRegistrations: OrganiserSelectors.registrationsEligibleForTravelGrant(state)
+});
+
+export default withSnackbar(connect(mapState)(CalculateSpend));
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Travel/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Travel/index.js
new file mode 100644
index 000000000..e78dbc207
--- /dev/null
+++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Travel/index.js
@@ -0,0 +1,171 @@
+import React, { useState, useEffect, useCallback } from 'react';
+
+import { sumBy } from 'lodash-es';
+import { connect } from 'react-redux';
+import { Typography, Grid } from '@material-ui/core';
+import { FilterHelpers } from '@hackjunction/shared';
+import { withSnackbar } from 'notistack';
+
+import Table from 'components/generic/Table';
+import TextInput from 'components/inputs/TextInput';
+import Statistic from 'components/generic/Statistic';
+
+import RegistrationsService from 'services/registrations';
+
+import * as AuthSelectors from 'redux/auth/selectors';
+import * as OrganiserSelectors from 'redux/organiser/selectors';
+
+import CalculateSpend from './CalculateSpend';
+
+const TravelGrantPage = ({
+ enqueueSnackbar,
+ event,
+ idToken,
+ registrations,
+ registrationsWithTravelGrant,
+ filterGroups,
+ filterGroupsLoading,
+ travelGrantSpend,
+ travelGrantCount,
+ travelGrantRejectedCount
+}) => {
+ const [groups, setGroups] = useState({});
+
+ useEffect(() => {
+ if (filterGroups) {
+ setGroups(
+ filterGroups.reduce((res, group) => {
+ res[group.label] = 0;
+ return res;
+ }, {})
+ );
+ }
+ }, [filterGroups]);
+
+ const handleAmountChange = useCallback(
+ (group, amount) => {
+ setGroups({
+ ...groups,
+ [group]: amount
+ });
+ },
+ [groups]
+ );
+
+ const filterGroupsMapped = filterGroups.map(group => {
+ const items = FilterHelpers.applyFilters(registrationsWithTravelGrant, group.filters);
+ return {
+ ...group,
+ spend: sumBy(items, r => r.travelGrant || 0)
+ };
+ });
+
+ const handleBulkReject = useCallback(() => {
+ return RegistrationsService.bulkRejectTravelGrantsForEvent(idToken, event.slug)
+ .then(() => {
+ enqueueSnackbar('Success!', { variant: 'success' });
+ return;
+ })
+ .catch(err => {
+ enqueueSnackbar('Something went wrong', { variant: 'error' });
+ return;
+ });
+ }, [idToken, event.slug, enqueueSnackbar]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Here you can automatically grant travel grants based on your filter groups. Set the amount you want
+ to grant for each group (or 0 to skip that group), and the tool will go through confirmed
+ participants in order of registration time, and assign them their respective travel grant amounts as
+ long as the budget is not exceeded.
+
+
+ If a participant belongs to more than one of your filter groups, they will be granted the travel
+ grant with the highest amount. If you want a group to be guaranteed to receive a travel grant,
+ regardless of how late they've registered, you can set all other groups' amounts to 0 and your
+ budget high enough to fit everyone in the group.
+
+
+ You can also assign travel grant amounts individually, by editing a given participant on the
+ Participants -tab.
+
+
+
+ (
+ handleAmountChange(label, value)}
+ />
+ )
+ }
+ ]}
+ />
+
+
+
+ Set budget and preview
+
+
+
+
+
+
+ );
+};
+
+const mapState = state => ({
+ idToken: AuthSelectors.getIdToken(state),
+ event: OrganiserSelectors.event(state),
+ registrations: OrganiserSelectors.registrationsEligibleForTravelGrant(state),
+ registrationsWithTravelGrant: OrganiserSelectors.registrationsWithTravelGrant(state),
+ filterGroups: OrganiserSelectors.filterGroups(state),
+ filterGroupsLoading: OrganiserSelectors.filterGroupsLoading(state),
+ travelGrantSpend: OrganiserSelectors.travelGrantSpend(state),
+ travelGrantCount: OrganiserSelectors.travelGrantCount(state),
+ travelGrantRejectedCount: OrganiserSelectors.travelGrantRejectedCount(state)
+});
+
+export default withSnackbar(connect(mapState)(TravelGrantPage));
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/index.js
new file mode 100644
index 000000000..d5c200659
--- /dev/null
+++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/index.js
@@ -0,0 +1,57 @@
+import React, { useState } from 'react';
+
+import { connect } from 'react-redux';
+import { Typography } from '@material-ui/core';
+
+import PageWrapper from 'components/PageWrapper';
+import Divider from 'components/generic/Divider';
+import MaterialTabsLayout from 'components/layouts/MaterialTabsLayout';
+import PageHeader from 'components/generic/PageHeader';
+
+import * as OrganiserSelectors from 'redux/organiser/selectors';
+
+import SearchAttendeesPage from './Search';
+import AssignAttendeesPage from './Assigned';
+import TeamsPage from './Teams';
+import AdminPage from './Admin';
+import TravelGrantsPage from './Travel';
+
+const OrganiserEditEventReview = ({ event, organisers, registrationsLoading, updateData }) => {
+ return (
+
+
+
+ },
+ {
+ label: 'Teams',
+ content:
+ },
+ {
+ label: 'Assigned to you',
+ content:
+ },
+ {
+ label: 'Travel',
+ content:
+ },
+ {
+ label: 'Admin & Tools',
+ content:
+ }
+ ]}
+ />
+
+ );
+};
+
+const mapState = state => ({
+ organisers: OrganiserSelectors.organisers(state),
+ event: OrganiserSelectors.event(state),
+ registrationsLoading: OrganiserSelectors.registrationsLoading(state)
+});
+
+export default connect(mapState)(OrganiserEditEventReview);
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventStats/OrganiserEditEventStats.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Stats/Stats.module.scss
similarity index 100%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventStats/OrganiserEditEventStats.module.scss
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Stats/Stats.module.scss
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventStats/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Stats/index.js
similarity index 77%
rename from frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventStats/index.js
rename to frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Stats/index.js
index 750dd16d6..904243b0b 100644
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/OrganiserEditEventStats/index.js
+++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Stats/index.js
@@ -1,32 +1,29 @@
import React, { useEffect } from 'react';
-import styles from './OrganiserEditEventStats.module.scss';
+import styles from './Stats.module.scss';
import { connect } from 'react-redux';
-import { PageHeader, Row, Col, Card } from 'antd';
+import { Row, Col, Card } from 'antd';
import * as OrganiserSelectors from 'redux/organiser/selectors';
import * as OrganiserActions from 'redux/organiser/actions';
import Divider from 'components/generic/Divider';
+import PageHeader from 'components/generic/PageHeader';
import PageWrapper from 'components/PageWrapper';
+
import ApplicationsOverTime from 'components/plots/ApplicationsOverTime';
import RatingsSplit from 'components/plots/RatingsSplit';
import ReviewersList from 'components/plots/ReviewersList';
-
import ApplicationsCount from 'components/plots/ApplicationsCount';
import TeamsCount from 'components/plots/TeamsCount';
import ReviewedPercent from 'components/plots/ReviewedPercent';
import ReviewedAverage from 'components/plots/ReviewedAverage';
import ApplicationsLast24h from 'components/plots/ApplicationsLast24h';
-const OrganiserEditEventStats = ({ slug, loading, updateRegistrations, updateTeams }) => {
- useEffect(() => {
- updateRegistrations(slug);
- updateTeams(slug);
- }, [slug, updateRegistrations, updateTeams]);
-
- const renderContent = () => {
- return (
+const OrganiserEditEventStats = ({ slug, loading }) => {
+ return (
+
+
@@ -77,12 +74,6 @@ const OrganiserEditEventStats = ({ slug, loading, updateRegistrations, updateTea
- );
- };
-
- return (
-
- Key stats for the event} footer={renderContent()} />
);
};
@@ -94,12 +85,4 @@ const mapState = state => ({
OrganiserSelectors.organisersLoading(state)
});
-const mapDispatch = dispatch => ({
- updateRegistrations: slug => dispatch(OrganiserActions.updateRegistrationsForEvent(slug)),
- updateTeams: slug => dispatch(OrganiserActions.updateTeamsForEvent(slug))
-});
-
-export default connect(
- mapState,
- mapDispatch
-)(OrganiserEditEventStats);
+export default connect(mapState)(OrganiserEditEventStats);
diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/index.js
index 9fcf64bd3..6cc93f811 100644
--- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/index.js
+++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/index.js
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import styles from './OrganiserEditEvent.module.scss';
import { connect } from 'react-redux';
@@ -9,31 +9,52 @@ import * as OrganiserActions from 'redux/organiser/actions';
import PageWrapper from 'components/PageWrapper';
import Image from 'components/generic/Image';
import EventNavBar from 'components/navbars/EventNavBar';
-import OrganiserEditEventDetails from './OrganiserEditEventDetails';
-import OrganiserEditEventStats from './OrganiserEditEventStats';
-import OrganiserEditEventReview from './OrganiserEditEventReview';
-import OrganiserEditEventManage from './OrganiserEditEventManage';
+
+import DetailsPage from './Details';
+import StatsPage from './Stats';
+import ParticipantsPage from './Participants';
+import ManagePage from './Manage';
import SidebarLayout from 'components/layouts/SidebarLayout';
-const OrganiserEditEvent = ({ updateEvent, updateOrganiserProfiles, event, user, match, location }) => {
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(false);
+const OrganiserEditEvent = ({
+ updateEvent,
+ updateOrganiserProfiles,
+ updateRegistrations,
+ updateTeams,
+ updateFilterGroups,
+ loading,
+ error,
+ event,
+ user,
+ match,
+ location
+}) => {
const { slug } = match.params;
useEffect(() => {
- setLoading(true);
- updateEvent(slug)
- .catch(err => {
- setError(true);
- })
- .finally(() => {
- setLoading(false);
- });
- }, [updateEvent, slug]);
+ updateEvent(slug);
+ }, [slug, updateEvent]);
+
+ const updateData = useCallback(() => {
+ if (event.owner) {
+ updateOrganiserProfiles(event.owner, event.organisers);
+ updateRegistrations(slug);
+ updateTeams(slug);
+ updateFilterGroups(slug);
+ }
+ }, [
+ slug,
+ event.owner,
+ event.organisers,
+ updateTeams,
+ updateRegistrations,
+ updateOrganiserProfiles,
+ updateFilterGroups
+ ]);
useEffect(() => {
- updateOrganiserProfiles(event.owner, event.organisers);
- }, [event.owner, event.organisers, updateOrganiserProfiles]);
+ updateData();
+ }, [updateData]);
return (
@@ -62,25 +83,25 @@ const OrganiserEditEvent = ({ updateEvent, updateOrganiserProfiles, event, user,
path: '',
icon: 'home',
label: 'Edit',
- render: routeProps =>
+ render: routeProps =>
},
{
path: '/stats',
icon: 'line-chart',
label: 'Stats',
- render: routeProps =>
+ render: routeProps =>
},
{
- path: '/review',
+ path: '/participants',
icon: 'star',
- label: 'Review',
- render: routeProps =>
+ label: 'Participants',
+ render: routeProps =>
},
{
path: '/manage',
icon: 'setting',
label: 'Manage',
- render: routeProps =>
+ render: routeProps =>
}
]}
/>
@@ -91,13 +112,18 @@ const OrganiserEditEvent = ({ updateEvent, updateOrganiserProfiles, event, user,
const mapStateToProps = state => ({
idToken: AuthSelectors.getIdToken(state),
user: AuthSelectors.getCurrentUser(state),
- event: OrganiserSelectors.event(state)
+ event: OrganiserSelectors.event(state),
+ loading: OrganiserSelectors.eventLoading(state),
+ error: OrganiserSelectors.eventError(state)
});
const mapDispatchToProps = dispatch => ({
updateEvent: slug => dispatch(OrganiserActions.updateEvent(slug)),
updateOrganiserProfiles: (owner, organisers) =>
- dispatch(OrganiserActions.updateOrganisersForEvent(owner, organisers))
+ dispatch(OrganiserActions.updateOrganisersForEvent(owner, organisers)),
+ updateRegistrations: slug => dispatch(OrganiserActions.updateRegistrationsForEvent(slug)),
+ updateTeams: slug => dispatch(OrganiserActions.updateTeamsForEvent(slug)),
+ updateFilterGroups: slug => dispatch(OrganiserActions.updateFilterGroups(slug))
});
export default connect(
diff --git a/frontend/src/redux/organiser/actionTypes.js b/frontend/src/redux/organiser/actionTypes.js
index d5a6192ab..1ba63684d 100644
--- a/frontend/src/redux/organiser/actionTypes.js
+++ b/frontend/src/redux/organiser/actionTypes.js
@@ -9,6 +9,10 @@ export const ADD_ORGANISER = 'organiser/ADD_ORGANISER';
export const UPDATE_REGISTRATIONS = 'organiser/UPDATE_REGISTRATIONS';
export const EDIT_REGISTRATION = 'organiser/EDIT_REGISTRATION';
-export const SET_REGISTRATIONS_FILTERS = 'organiser/SET_REGISTRATIONS_FILTERS';
export const UPDATE_TEAMS = 'organiser/UPDATE_TEAMS';
+
+export const UPDATE_FILTER_GROUPS = 'organiser/UPDATE_FILTER_GROUPS';
+export const CREATE_FILTER_GROUP = 'organiser/CREATE_FILTER_GROUP';
+export const EDIT_FILTER_GROUP = 'organiser/EDIT_FILTER_GROUP';
+export const DELETE_FILTER_GROUP = 'organiser/DELETE_FILTER_GROUP';
diff --git a/frontend/src/redux/organiser/actions.js b/frontend/src/redux/organiser/actions.js
index 23fdd7050..025287b7c 100644
--- a/frontend/src/redux/organiser/actions.js
+++ b/frontend/src/redux/organiser/actions.js
@@ -4,6 +4,7 @@ import UserProfilesService from 'services/userProfiles';
import EventsService from 'services/events';
import RegistrationsService from 'services/registrations';
import TeamsService from 'services/teams';
+import FilterGroupsService from 'services/filterGroups';
/** Update event with loading/error data */
export const updateEvent = slug => async (dispatch, getState) => {
@@ -128,10 +129,56 @@ export const updateTeamsForEvent = slug => async (dispatch, getState) => {
});
};
-/** Set filters for attendees table */
-export const setRegistrationsFilters = filters => dispatch => {
+/** Update filter groups with loading/error status */
+export const updateFilterGroups = slug => async (dispatch, getState) => {
+ const idToken = AuthSelectors.getIdToken(getState());
+
+ dispatch({
+ type: ActionTypes.UPDATE_FILTER_GROUPS,
+ promise: FilterGroupsService.getFilterGroupsForEvent(idToken, slug),
+ meta: {
+ onFailure: e => console.log('Error updating filter groups', e)
+ }
+ });
+
+ return;
+};
+
+export const createFilterGroup = (slug, label, description, filters) => async (dispatch, getState) => {
+ const idToken = AuthSelectors.getIdToken(getState());
+
+ const filterGroup = await FilterGroupsService.createFilterGroup(idToken, label, description, filters, slug);
+
+ dispatch({
+ type: ActionTypes.CREATE_FILTER_GROUP,
+ payload: filterGroup
+ });
+
+ return filterGroup;
+};
+
+export const editFilterGroup = (slug, label, description, filters) => async (dispatch, getState) => {
+ const idToken = AuthSelectors.getIdToken(getState());
+
+ const filterGroup = await FilterGroupsService.editFilterGroup(idToken, label, description, filters, slug);
+
dispatch({
- type: ActionTypes.SET_REGISTRATIONS_FILTERS,
- payload: filters
+ type: ActionTypes.EDIT_FILTER_GROUP,
+ payload: filterGroup
});
+
+ return filterGroup;
+};
+
+export const deleteFilterGroup = (slug, label) => async (dispatch, getState) => {
+ const idToken = AuthSelectors.getIdToken(getState());
+
+ const filterGroup = await FilterGroupsService.deleteFilterGroup(idToken, label, slug);
+
+ dispatch({
+ type: ActionTypes.DELETE_FILTER_GROUP,
+ payload: filterGroup
+ });
+
+ return filterGroup;
};
diff --git a/frontend/src/redux/organiser/reducer.js b/frontend/src/redux/organiser/reducer.js
index e636ee673..7fffad80f 100644
--- a/frontend/src/redux/organiser/reducer.js
+++ b/frontend/src/redux/organiser/reducer.js
@@ -28,14 +28,19 @@ const initialState = {
error: false,
updated: 0,
data: [],
- map: {},
- filters: []
+ map: {}
},
teams: {
loading: false,
error: false,
updated: 0,
data: []
+ },
+ filterGroups: {
+ loading: false,
+ error: false,
+ updated: 0,
+ data: []
}
};
@@ -44,6 +49,7 @@ const eventHandler = buildHandler('event');
const statsHandler = buildHandler('stats');
const organisersHandler = buildHandler('organisers', 'userId');
const registrationsHandler = buildHandler('registrations', 'user');
+const filterGroupsHandler = buildHandler('filterGroups');
const teamsHandler = buildHandler('teams');
const editEvent = buildUpdatePath('event.data');
const editEventOrganisers = buildUpdatePath('event.data.organisers');
@@ -68,6 +74,46 @@ export default function reducer(state = initialState, action) {
case ActionTypes.UPDATE_TEAMS: {
return teamsHandler(state, action);
}
+ case ActionTypes.UPDATE_FILTER_GROUPS: {
+ return filterGroupsHandler(state, action);
+ }
+ case ActionTypes.CREATE_FILTER_GROUP: {
+ return {
+ ...state,
+ filterGroups: {
+ ...state.filterGroups,
+ data: state.filterGroups.data.concat(action.payload)
+ }
+ };
+ }
+ case ActionTypes.EDIT_FILTER_GROUP: {
+ return {
+ ...state,
+ filterGroups: {
+ ...state.filterGroups,
+ data: state.filterGroups.data.map(filterGroup => {
+ if (filterGroup.label === action.payload.label) {
+ return action.payload;
+ }
+ return filterGroup;
+ })
+ }
+ };
+ }
+ case ActionTypes.DELETE_FILTER_GROUP: {
+ return {
+ ...state,
+ filterGroups: {
+ ...state.filterGroups,
+ data: state.filterGroups.data.filter(filterGroup => {
+ if (filterGroup.label === action.payload.label) {
+ return false;
+ }
+ return true;
+ })
+ }
+ };
+ }
case ActionTypes.EDIT_REGISTRATION: {
const registration = action.payload;
return {
@@ -87,15 +133,6 @@ export default function reducer(state = initialState, action) {
}
};
}
- case ActionTypes.SET_REGISTRATIONS_FILTERS: {
- return {
- ...state,
- registrations: {
- ...state.registrations,
- filters: action.payload
- }
- };
- }
case ActionTypes.REMOVE_ORGANISER: {
const data = filter(state.event.data.organisers, userId => {
return userId !== action.payload;
diff --git a/frontend/src/redux/organiser/selectors.js b/frontend/src/redux/organiser/selectors.js
index 706f47683..973431389 100644
--- a/frontend/src/redux/organiser/selectors.js
+++ b/frontend/src/redux/organiser/selectors.js
@@ -1,6 +1,6 @@
import { createSelector } from 'reselect';
-import { meanBy, countBy, groupBy, mapValues } from 'lodash-es';
-import * as FilterUtils from 'utils/filters';
+import { meanBy, countBy, groupBy, mapValues, sumBy } from 'lodash-es';
+import { RegistrationStatuses } from '@hackjunction/shared';
import * as AuthSelectors from 'redux/auth/selectors';
import moment from 'moment';
@@ -25,15 +25,16 @@ export const registrationsMap = state => state.organiser.registrations.map;
export const registrationsLoading = state => state.organiser.registrations.loading;
export const registrationsError = state => state.organiser.registrations.error;
export const registrationsUpdated = state => state.organiser.registrations.updated;
-export const registrationsFilters = state => state.organiser.registrations.filters;
-export const registrationsFiltered = createSelector(
- registrations,
- registrationsFilters,
- (registrations, filters) => {
- return FilterUtils.applyFilters(registrations, filters);
- }
-);
+export const teams = state => state.organiser.teams.data;
+export const teamsLoading = state => state.organiser.teams.loading;
+export const teamsError = state => state.organiser.teams.error;
+export const teamsUpdated = state => state.organiser.teams.updated;
+
+export const filterGroups = state => state.organiser.filterGroups.data;
+export const filterGroupsLoading = state => state.organiser.filterGroups.loading;
+export const filterGroupsError = state => state.organiser.filterGroups.error;
+export const filterGroupsUpdated = state => state.organiser.filterGroups.updated;
export const registrationsAssigned = createSelector(
AuthSelectors.getCurrentUser,
@@ -54,10 +55,50 @@ export const registrationsReviewed = createSelector(
}
);
-export const teams = state => state.organiser.teams.data;
-export const teamsLoading = state => state.organiser.teams.loading;
-export const teamsError = state => state.organiser.teams.error;
-export const teamsUpdated = state => state.organiser.teams.updated;
+export const registrationsConfirmed = createSelector(
+ registrations,
+ registrations => {
+ const validStatuses = [RegistrationStatuses.asObject.confirmed.id, RegistrationStatuses.asObject.checkedIn.id];
+ return registrations.filter(registration => {
+ return validStatuses.indexOf(registration.status) !== -1;
+ });
+ }
+);
+
+export const registrationsEligibleForTravelGrant = createSelector(
+ registrationsConfirmed,
+ registrations =>
+ registrations.filter(r => {
+ return !r.travelGrant && r.travelGrant !== 0 && r.answers.needsTravelGrant;
+ })
+);
+
+export const registrationsWithTravelGrant = createSelector(
+ registrationsConfirmed,
+ registrations =>
+ registrations.filter(r => {
+ return r.travelGrant && r.travelGrant !== 0;
+ })
+);
+
+export const travelGrantSpend = createSelector(
+ registrationsWithTravelGrant,
+ registrations => {
+ return sumBy(registrations, r => {
+ return r.travelGrant || 0;
+ });
+ }
+);
+
+export const travelGrantCount = createSelector(
+ registrationsWithTravelGrant,
+ registrations => registrations.length
+);
+
+export const travelGrantRejectedCount = createSelector(
+ registrationsConfirmed,
+ registrations => registrations.filter(r => r.travelGrant === 0).length
+);
export const teamsPopulated = createSelector(
registrationsMap,
diff --git a/frontend/src/services/filterGroups.js b/frontend/src/services/filterGroups.js
new file mode 100644
index 000000000..8bb19851f
--- /dev/null
+++ b/frontend/src/services/filterGroups.js
@@ -0,0 +1,40 @@
+import _axios from 'services/axios';
+
+const FilterGroupsService = {};
+
+function config(idToken) {
+ return {
+ headers: {
+ Authorization: `Bearer ${idToken}`
+ }
+ };
+}
+
+FilterGroupsService.createFilterGroup = (idToken, label, description, filters, eventSlug) => {
+ const data = {
+ label,
+ description,
+ filters
+ };
+ return _axios.post(`/filter-groups/${eventSlug}`, data, config(idToken));
+};
+
+FilterGroupsService.editFilterGroup = (idToken, label, description, filters, eventSlug) => {
+ const data = {
+ label,
+ description,
+ filters
+ };
+ return _axios.patch(`/filter-groups/${eventSlug}`, data, config(idToken));
+};
+
+FilterGroupsService.deleteFilterGroup = (idToken, label, eventSlug) => {
+ const data = { label };
+ return _axios.delete(`/filter-groups/${eventSlug}`, { ...config(idToken), data });
+};
+
+FilterGroupsService.getFilterGroupsForEvent = (idToken, eventSlug) => {
+ return _axios.get(`/filter-groups/${eventSlug}`, config(idToken));
+};
+
+export default FilterGroupsService;
diff --git a/frontend/src/services/registrations.js b/frontend/src/services/registrations.js
index 16297882c..c63e32f48 100644
--- a/frontend/src/services/registrations.js
+++ b/frontend/src/services/registrations.js
@@ -68,6 +68,17 @@ RegistrationsService.bulkEditRegistrationsForEvent = (idToken, slug, registratio
return _axios.patch(`${BASE_ROUTE}/${slug}/bulk`, { registrationIds, edits }, config(idToken));
};
+/** Assign travel grant amounts in bulk
+ * PATCH /:slug/bulk/grants
+ */
+RegistrationsService.bulkAssignTravelGrantsForEvent = (idToken, slug, grants) => {
+ return _axios.patch(`${BASE_ROUTE}/${slug}/bulk/grants`, { grants }, config(idToken));
+};
+
+RegistrationsService.bulkRejectTravelGrantsForEvent = (idToken, slug) => {
+ return _axios.delete(`${BASE_ROUTE}/${slug}/bulk/grants`, config(idToken));
+};
+
/** Accept all soft-accepted registrations
* PATCH /:slug/bulk/accept
*/
diff --git a/frontend/src/styles/main.scss b/frontend/src/styles/main.scss
index e624712bc..70ba590f4 100644
--- a/frontend/src/styles/main.scss
+++ b/frontend/src/styles/main.scss
@@ -24,6 +24,7 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 16px;
+ padding: 0 !important;
}
body.body {
@@ -39,40 +40,9 @@ body.body {
flex-direction: column;
}
-/* Define sensible defaults for common elements */
+/* Define some overrides */
.body {
- h1,
- h2,
- h3,
- h4,
- h5,
- h6 {
- @extend %font-title;
- }
-
- p,
- span,
- li,
- td,
- th {
- @extend %font-body;
- }
-
- label,
- small {
- @extend %font-secondary;
- }
-
- input,
- textarea {
- @extend %font-input;
- }
-
- a {
- @extend %font-link;
- }
-
.ant-timeline-item-head-custom {
background: $lightgrey;
}
diff --git a/package-lock.json b/package-lock.json
index cca688d1f..9e53cef44 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -93,6 +93,12 @@
"concat-map": "0.0.1"
}
},
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+ "dev": true
+ },
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -134,11 +140,29 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
+ "concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
"core-js": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A=="
},
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
+ "dev": true
+ },
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -371,6 +395,12 @@
}
}
},
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -407,6 +437,12 @@
"has-symbols": "^1.0.0"
}
},
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+ "dev": true
+ },
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -463,6 +499,16 @@
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
+ "lru-cache": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
+ "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
+ "dev": true,
+ "requires": {
+ "pseudomap": "^1.0.2",
+ "yallist": "^2.1.2"
+ }
+ },
"map-stream": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
@@ -533,6 +579,12 @@
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true
},
+ "os-shim": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz",
+ "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=",
+ "dev": true
+ },
"parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
@@ -589,6 +641,45 @@
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
},
+ "pre-commit": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/pre-commit/-/pre-commit-1.2.2.tgz",
+ "integrity": "sha1-287g7p3nI15X95xW186UZBpp7sY=",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^5.0.1",
+ "spawn-sync": "^1.0.15",
+ "which": "1.2.x"
+ },
+ "dependencies": {
+ "cross-spawn": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+ "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^4.0.1",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "which": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz",
+ "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ }
+ }
+ },
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
+ },
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
@@ -607,6 +698,12 @@
"event-stream": "=3.3.4"
}
},
+ "pseudomap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+ "dev": true
+ },
"raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
@@ -669,6 +766,21 @@
"path-type": "^3.0.0"
}
},
+ "readable-stream": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
"recharts": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-1.7.1.tgz",
@@ -746,6 +858,12 @@
"path-parse": "^1.0.6"
}
},
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
@@ -778,6 +896,16 @@
"jsonify": "~0.0.0"
}
},
+ "spawn-sync": {
+ "version": "1.0.15",
+ "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz",
+ "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=",
+ "dev": true,
+ "requires": {
+ "concat-stream": "^1.4.7",
+ "os-shim": "^0.1.2"
+ }
+ },
"spdx-correct": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
@@ -837,6 +965,15 @@
"function-bind": "^1.0.2"
}
},
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -857,6 +994,18 @@
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
},
+ "typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
+ "dev": true
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+ "dev": true
+ },
"validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -874,6 +1023,12 @@
"requires": {
"isexe": "^2.0.0"
}
+ },
+ "yallist": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
+ "dev": true
}
}
}
diff --git a/package.json b/package.json
index 14b85ab07..025d28272 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,10 @@
"dev:backend": "cd backend && npm run dev",
"db-sync:local": "sh ./scripts/sync-production-to-local.sh",
"db-sync:dev": "sh ./scripts/sync-production-to-dev.sh",
- "db-sync:staging": "sh ./scripts/sync-production-to-staging.sh"
+ "db-sync:staging": "sh ./scripts/sync-production-to-staging.sh",
+ "pre-commit:frontend": "cd frontend && npm run precommit",
+ "pre-commit:backend": "cd backend && npm run precommit",
+ "pre-commit:shared": "cd shared && npm run precommit"
},
"betterScripts": {
"setup": "better-npm-run setup:backend && better-npm-run setup:frontend",
@@ -23,10 +26,16 @@
"start:prod": "cd backend && npm start",
"build": "rm -rf ./backend/build && cd frontend && npm run build && cp -r ./build ../backend/build"
},
+ "pre-commit": [
+ "pre-commit:frontend",
+ "pre-commit:backend",
+ "pre-commit:shared"
+ ],
"author": "Juuso Lappalainen",
"license": "ISC",
"devDependencies": {
- "npm-run-all": "^4.1.5"
+ "npm-run-all": "^4.1.5",
+ "pre-commit": "^1.2.2"
},
"dependencies": {
"better-npm-run": "^0.1.1",
diff --git a/pre-commit.sample b/pre-commit.sample
deleted file mode 100755
index d6c7dbc7d..000000000
--- a/pre-commit.sample
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/bin/sh
-#
-# An example hook script to verify what is about to be committed.
-# Called by "git commit" with no arguments. The hook should
-# exit with non-zero status after issuing an appropriate message if
-# it wants to stop the commit.
-#
-# To enable this hook, copy this file to .git/hooks/pre-commit
-
-if [ -z "$(git status shared --porcelain)" ]; then
- echo "[PRE-COMMIT] No changes in /shared"
-else
- echo "[PRE-COMMIT] Found changes in shared!"
- echo "[PRE-COMMIT] Publishing new version of @hackjunction/shared"
- cd shared && npm version patch && npm publish --silent
- echo "[PRE-COMMIT] Sleeping for 10s (wait for npm to publish package)"
- sleep 10s
- echo "[PRE-COMMIT] Upgrading backend to latest version of @hackjunction/shared"
- cd ../backend && npm upgrade @hackjunction/shared
- echo "[PRE-COMMIT] Upgrading frontend to latest version of @hackjunction/shared"
- cd ../frontend && npm upgrade @hackjunction/shared
- cd ..
- echo "[PRE-COMMIT] Adding all related changes to commit"
- git add backend/package.json ./backend/package-lock.json
- git add frontend/package.json ./frontend/package-lock.json
- git add shared/package.json ./shared/package-lock.json
- echo "[PRE-COMMIT] Done."
-fi
-exit 0
diff --git a/shared/constants/field-types.js b/shared/constants/field-types.js
new file mode 100644
index 000000000..5a7aaa127
--- /dev/null
+++ b/shared/constants/field-types.js
@@ -0,0 +1,70 @@
+const FieldTypes = {
+ SHORT_TEXT: {
+ id: 'SHORT_TEXT'
+ },
+ LONG_TEXT: {
+ id: 'LONG_TEXT'
+ },
+ BOOLEAN: {
+ id: 'BOOLEAN'
+ },
+ EMAIL: {
+ id: 'EMAIL'
+ },
+ PHONE_NUMBER: {
+ id: 'PHONE_NUMBER'
+ },
+ DATE: {
+ id: 'DATE'
+ },
+ GENDER: {
+ id: 'GENDER'
+ },
+ NATIONALITY: {
+ id: 'NATIONALITY'
+ },
+ LANGUAGES: {
+ id: 'LANGUAGES'
+ },
+ COUNTRY: {
+ id: 'COUNTRY'
+ },
+ ROLES: {
+ id: 'ROLES'
+ },
+ SKILLS: {
+ id: 'SKILLS'
+ },
+ INDUSTRIES: {
+ id: 'INDUSTRIES'
+ },
+ THEMES: {
+ id: 'THEMES'
+ },
+ EDUCATION: {
+ id: 'EDUCATION'
+ },
+ SMALL_NUMBER: {
+ id: 'SMALL_NUMBER'
+ },
+ NUM_HACKATHONS: {
+ id: 'NUM_HACKATHONS'
+ },
+ T_SHIRT_SIZE: {
+ id: 'T_SHIRT_SIZE'
+ },
+ URL: {
+ id: 'URL'
+ },
+ DIETARY_RESTRICTIONS: {
+ id: 'DIETARY_RESTRICTIONS'
+ },
+ TEAM_OPTIONS: {
+ id: 'TEAM_OPTIONS'
+ },
+ RECRUITMENT_OPTIONS: {
+ id: 'RECRUITMENT_OPTIONS'
+ }
+};
+
+module.exports = FieldTypes;
diff --git a/shared/constants/filter-types.js b/shared/constants/filter-types.js
new file mode 100644
index 000000000..3bcae4ece
--- /dev/null
+++ b/shared/constants/filter-types.js
@@ -0,0 +1,134 @@
+const filterTypes = {
+ IS_EMPTY: {
+ id: 'IS_EMPTY',
+ label: 'Is empty'
+ },
+ NOT_EMPTY: {
+ id: 'NOT_EMPTY',
+ label: "Isn't empty"
+ },
+ EQUALS: {
+ id: 'EQUALS',
+ label: 'Is equal to'
+ },
+ NOT_EQUALS: {
+ id: 'NOT_EQUALS',
+ label: "Isn't equal to"
+ },
+ ONE_OF: {
+ id: 'ONE_OF',
+ label: 'Is one of'
+ },
+ NOT_ONE_OF: {
+ id: 'NOT_ONE_OF',
+ label: "Isn't one of "
+ },
+ CONTAINS: {
+ id: 'CONTAINS',
+ label: 'Contains'
+ },
+ NOT_CONTAINS: {
+ id: 'NOT_CONTAINS',
+ label: "Doesn't contain"
+ },
+ CONTAINS_ONE_OF: {
+ id: 'CONTAINS_ONE_OF',
+ label: 'Contains one of'
+ },
+ NOT_CONTAINS_ONE_OF: {
+ id: 'NOT_CONTAINS_ONE_OF',
+ label: "Doesn't contain one of"
+ },
+ LESS_THAN: {
+ id: 'LESS_THAN',
+ label: 'Is less than',
+ helper: 'Or length is less than'
+ },
+ NOT_LESS_THAN: {
+ id: 'NOT_LESS_THAN',
+ label: 'Is at least',
+ helper: 'Or length is at least'
+ },
+ MORE_THAN: {
+ id: 'MORE_THAN',
+ label: 'Is more than',
+ helper: 'Or length is more than'
+ },
+ NOT_MORE_THAN: {
+ id: 'NOT_MORE_THAN',
+ label: 'Is at most',
+ helper: 'Or length is at most'
+ },
+ BOOLEAN_TRUE: {
+ id: 'BOOLEAN_TRUE',
+ label: 'Yes'
+ },
+ BOOLEAN_FALSE: {
+ id: 'BOOLEAN_FALSE',
+ label: 'No',
+ helper: 'Or unanswered'
+ }
+};
+
+const stringFilterTypes = [
+ filterTypes.IS_EMPTY.id,
+ filterTypes.NOT_EMPTY.id,
+ filterTypes.EQUALS.id,
+ filterTypes.NOT_EQUALS.id,
+ filterTypes.CONTAINS.id,
+ filterTypes.NOT_CONTAINS.id,
+ filterTypes.ONE_OF.id,
+ filterTypes.NOT_ONE_OF.id,
+ filterTypes.LESS_THAN.id,
+ filterTypes.NOT_LESS_THAN.id,
+ filterTypes.MORE_THAN.id,
+ filterTypes.NOT_MORE_THAN.id
+];
+
+const arrayFilterTypes = [
+ filterTypes.IS_EMPTY.id,
+ filterTypes.NOT_EMPTY.id,
+ filterTypes.CONTAINS.id,
+ filterTypes.NOT_CONTAINS.id,
+ filterTypes.LESS_THAN.id,
+ filterTypes.NOT_LESS_THAN.id,
+ filterTypes.MORE_THAN.id,
+ filterTypes.NOT_MORE_THAN.id,
+ filterTypes.CONTAINS_ONE_OF.id,
+ filterTypes.NOT_CONTAINS_ONE_OF.id
+];
+
+const numberFilterTypes = [
+ filterTypes.IS_EMPTY.id,
+ filterTypes.NOT_EMPTY.id,
+ filterTypes.EQUALS.id,
+ filterTypes.NOT_EQUALS.id,
+ filterTypes.LESS_THAN.id,
+ filterTypes.NOT_LESS_THAN.id,
+ filterTypes.MORE_THAN.id,
+ filterTypes.NOT_MORE_THAN.id
+];
+
+const booleanFilterTypes = [filterTypes.BOOLEAN_TRUE, filterTypes.BOOLEAN_FALSE];
+
+const STRING = 'STRING';
+const ARRAY = 'ARRAY';
+const NUMBER = 'NUMBER';
+const BOOLEAN = 'BOOLEAN';
+const DATE = 'DATE';
+
+module.exports = {
+ filterTypes,
+ filterTypesForType: {
+ STRING: stringFilterTypes,
+ ARRAY: arrayFilterTypes,
+ NUMBER: numberFilterTypes,
+ BOOLEAN: booleanFilterTypes,
+ DATE: []
+ },
+ STRING,
+ ARRAY,
+ NUMBER,
+ BOOLEAN,
+ DATE
+};
diff --git a/shared/constants/filter-values.js b/shared/constants/filter-values.js
new file mode 100644
index 000000000..61f790a06
--- /dev/null
+++ b/shared/constants/filter-values.js
@@ -0,0 +1,15 @@
+const FilterValues = {
+ STRING: 'STRING',
+ BOOLEAN: 'BOOLEAN',
+ NUMBER: 'NUMBER',
+ RATING: 'RATING',
+ DATE: 'DATE',
+ GENDER: 'GENDER',
+ NATIONALITY: 'NATIONALITY',
+ COUNTRY: 'COUNTRY',
+ LANGUAGE: 'LANGUAGE',
+ STATUS: 'STATUS',
+ TAG: 'TAG'
+};
+
+module.exports = FilterValues;
diff --git a/shared/constants/misc.js b/shared/constants/misc.js
index 7d7c3c49f..ccccf9c69 100644
--- a/shared/constants/misc.js
+++ b/shared/constants/misc.js
@@ -57,6 +57,21 @@ const relocationOptions = {
}
};
+const travelGrantStatuses = {
+ accepted: {
+ id: 'accepted',
+ label: 'Accepted'
+ },
+ rejected: {
+ id: 'rejected',
+ label: 'Rejected'
+ },
+ confirmed: {
+ id: 'confirmed',
+ label: 'Confirmed'
+ }
+};
+
const dietaryRestrictions = [
'Vegan',
'Vegetarian',
@@ -91,6 +106,13 @@ Misc.relocationOptions = {
getLabelForValue: value => (relocationOptions.hasOwnProperty(value) ? relocationOptions[value].label : '')
};
+Misc.travelGrantStatuses = {
+ items: travelGrantStatuses,
+ ids: Object.keys(travelGrantStatuses),
+ asArray: Object.keys(travelGrantStatuses).map(id => travelGrantStatuses[id]),
+ getLabelForValue: value => (travelGrantStatuses.hasOwnProperty(value) ? travelGrantStatuses[value].label : '')
+};
+
Misc.tShirtSizes = tShirtSizes;
Misc.dietaryRestrictions = dietaryRestrictions;
diff --git a/shared/constants/registration-fields.js b/shared/constants/registration-fields.js
index 95df860d3..051f5a239 100644
--- a/shared/constants/registration-fields.js
+++ b/shared/constants/registration-fields.js
@@ -6,6 +6,9 @@ const Themes = require('../constants/themes');
const Roles = require('../constants/roles');
const Skills = require('../constants/skills');
const Misc = require('../constants/misc');
+const FieldTypes = require('./field-types');
+const FilterTypes = require('./filter-types');
+const FilterValues = require('./filter-values');
const Categories = {
basicDetails: {
@@ -34,75 +37,6 @@ const Categories = {
}
};
-const FieldTypes = {
- SHORT_TEXT: {
- id: 'SHORT_TEXT'
- },
- LONG_TEXT: {
- id: 'LONG_TEXT'
- },
- BOOLEAN: {
- id: 'BOOLEAN'
- },
- EMAIL: {
- id: 'EMAIL'
- },
- PHONE_NUMBER: {
- id: 'PHONE_NUMBER'
- },
- DATE: {
- id: 'DATE'
- },
- GENDER: {
- id: 'GENDER'
- },
- NATIONALITY: {
- id: 'NATIONALITY'
- },
- LANGUAGES: {
- id: 'LANGUAGES'
- },
- COUNTRY: {
- id: 'COUNTRY'
- },
- ROLES: {
- id: 'ROLES'
- },
- SKILLS: {
- id: 'SKILLS'
- },
- INDUSTRIES: {
- id: 'INDUSTRIES'
- },
- THEMES: {
- id: 'THEMES'
- },
- EDUCATION: {
- id: 'EDUCATION'
- },
- SMALL_NUMBER: {
- id: 'SMALL_NUMBER'
- },
- NUM_HACKATHONS: {
- id: 'NUM_HACKATHONS'
- },
- T_SHIRT_SIZE: {
- id: 'T_SHIRT_SIZE'
- },
- URL: {
- id: 'URL'
- },
- DIETARY_RESTRICTIONS: {
- id: 'DIETARY_RESTRICTIONS'
- },
- TEAM_OPTIONS: {
- id: 'TEAM_OPTIONS'
- },
- RECRUITMENT_OPTIONS: {
- id: 'RECRUITMENT_OPTIONS'
- }
-};
-
const FieldProps = {
firstName: {
label: 'First name',
@@ -120,7 +54,15 @@ const FieldProps = {
defaultEnable: true,
defaultRequire: true,
editable: false
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'First name',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.STRING
+ }
+ ]
},
lastName: {
label: 'Last name',
@@ -138,7 +80,15 @@ const FieldProps = {
defaultEnable: true,
defaultRequire: true,
editable: false
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Last name',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.STRING
+ }
+ ]
},
email: {
label: 'Email',
@@ -155,7 +105,15 @@ const FieldProps = {
defaultEnable: true,
defaultRequire: true,
editable: false
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Email',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.STRING
+ }
+ ]
},
phoneNumber: {
label: 'Phone number',
@@ -188,7 +146,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Date of birth',
+ type: FilterTypes.DATE,
+ valueType: FilterValues.DATE
+ }
+ ]
},
gender: {
label: 'Gender',
@@ -203,7 +169,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Gender',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.GENDER
+ }
+ ]
},
nationality: {
label: 'Nationality',
@@ -218,7 +192,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Nationality',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.NATIONALITY
+ }
+ ]
},
spokenLanguages: {
label: 'Spoken languages',
@@ -235,7 +217,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Spoken languages',
+ type: FilterTypes.ARRAY,
+ valueType: FilterValues.LANGUAGE
+ }
+ ]
},
countryOfResidence: {
label: 'Country of residence',
@@ -250,7 +240,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Country of Residence',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.COUNTRY
+ }
+ ]
},
cityOfResidence: {
label: 'City of residence',
@@ -424,7 +422,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequired: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Motivation',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.STRING
+ }
+ ]
},
portfolio: {
label: 'Link to Portfolio',
@@ -440,7 +446,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Link to Portfolio',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.STRING
+ }
+ ]
},
github: {
label: 'Link to Github',
@@ -456,7 +470,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Link to GitHub',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.STRING
+ }
+ ]
},
linkedin: {
label: 'LinkedIn Profile',
@@ -471,7 +493,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'LinkedIn profile',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.STRING
+ }
+ ]
},
countryOfTravel: {
label: 'Country of Travel',
@@ -483,7 +513,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Country of Travel',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.COUNTRY
+ }
+ ]
},
cityOfTravel: {
label: 'City of Travel',
@@ -494,7 +532,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'City of Travel',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.STRING
+ }
+ ]
},
needsVisa: {
label: 'Do you need a visa?',
@@ -506,7 +552,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Link to Portfolio',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.STRING
+ }
+ ]
},
needsTravelGrant: {
label: 'Do you want to apply for a travel grant?',
@@ -523,7 +577,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Applied for travel grant',
+ type: FilterTypes.BOOLEAN,
+ valueType: FilterValues.BOOLEAN
+ }
+ ]
},
needsAccommodation: {
label: 'Do you need free accommodation?',
@@ -535,7 +597,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Needs accommodation',
+ type: FilterTypes.BOOLEAN,
+ valueType: FilterValues.BOOLEAN
+ }
+ ]
},
recruitmentOptions: {
label: 'Job opportunities',
@@ -563,7 +633,21 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: 'applyAsTeam',
+ label: 'Team > Applied as team',
+ type: FilterTypes.BOOLEAN,
+ valueType: FilterValues.BOOLEAN
+ },
+ {
+ path: 'applyAlone',
+ label: 'Team > Applied also alone',
+ type: FilterTypes.BOOLEAN,
+ valueType: FilterValues.BOOLEAN
+ }
+ ]
},
secretCode: {
label: 'Secret code',
@@ -575,7 +659,15 @@ const FieldProps = {
defaultEnable: false,
defaultRequire: false,
editable: true
- }
+ },
+ filters: [
+ {
+ path: '',
+ label: 'Secret Code',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.STRING
+ }
+ ]
}
};
@@ -978,6 +1070,45 @@ function buildFieldToLabelMap() {
return result;
}
+function buildFiltersArray() {
+ const fields = Object.keys(Fields);
+ const baseFilters = [
+ {
+ path: 'rating',
+ label: 'Rating',
+ type: FilterTypes.NUMBER,
+ valueType: FilterValues.NUMBER
+ },
+ {
+ path: 'status',
+ label: 'Status',
+ type: FilterTypes.STRING,
+ valueType: FilterValues.STATUS
+ },
+ {
+ path: 'tags',
+ label: 'Tags',
+ type: FilterTypes.ARRAY,
+ valueType: FilterValues.TAG
+ }
+ ];
+ const answerFilters = fields.reduce((res, fieldKey) => {
+ const field = Fields[fieldKey];
+ if (!Array.isArray(field.filters) || !field.filters.length) return res;
+ const filters = field.filters.map(filter => {
+ if (filter.path.length) {
+ filter.path = `answers.${fieldKey}.${filter.path}`;
+ } else {
+ filter.path = `answers.${fieldKey}`;
+ }
+ return filter;
+ });
+ return res.concat(filters);
+ }, []);
+
+ return baseFilters.concat(answerFilters);
+}
+
const Helpers = {
getLabel: field => {
if (Fields.hasOwnProperty(field)) {
@@ -987,6 +1118,8 @@ const Helpers = {
},
getFields: () => Fields,
getField: field => Fields[field],
+ getFieldType: field => (Fields[field] ? Fields[field].fieldType.id : null),
+ filters: buildFiltersArray(),
fieldToLabelMap: buildFieldToLabelMap(),
fieldTypes: FieldTypes,
getCategory: field => {
diff --git a/shared/constants/select-options.js b/shared/constants/select-options.js
new file mode 100644
index 000000000..f9101679b
--- /dev/null
+++ b/shared/constants/select-options.js
@@ -0,0 +1,49 @@
+const Countries = require('./countries');
+const Genders = require('./genders');
+const Industries = require('./industries');
+const Languages = require('./languages');
+const Roles = require('./roles');
+const Skills = require('./skills');
+const Themes = require('./themes');
+const RegistrationStatuses = require('./registration-statuses');
+
+const SelectOptions = {
+ COUNTRIES: Countries.asArrayOfName.map(country => ({
+ label: country,
+ value: country
+ })),
+ NATIONALITIES: Countries.asArrayOfNationalities.map(nationality => ({
+ label: nationality,
+ value: nationality
+ })),
+ GENDERS: Genders.map(gender => ({
+ label: gender,
+ value: gender
+ })),
+ INDUSTRIES: Industries.industries.map(industry => ({
+ label: industry,
+ value: industry
+ })),
+ LANGUAGES: Languages.asArrayOfNames.map(language => ({
+ label: language,
+ value: language
+ })),
+ ROLES: Roles.items.map(role => ({
+ label: role,
+ value: role
+ })),
+ SKILLS: Skills.items.map(skill => ({
+ label: skill,
+ value: skill
+ })),
+ THEMES: Themes.themes.map(theme => ({
+ label: theme,
+ value: theme
+ })),
+ STATUSES: RegistrationStatuses.asArray.map(status => ({
+ label: status.label,
+ value: status.id
+ }))
+};
+
+module.exports = SelectOptions;
diff --git a/shared/helpers/customValidator.js b/shared/helpers/customValidator.js
index 0b022b58e..09fd1ef70 100644
--- a/shared/helpers/customValidator.js
+++ b/shared/helpers/customValidator.js
@@ -15,7 +15,7 @@ class CustomValidator extends BaseValidator {
return this.joi
.string()
.min(fieldOptions.minLength || 0)
- .max(fieldOptions.maxLength || 100)
+ .max(fieldOptions.maxLength || 10000)
.allow(...allowArgs)
.label(fieldLabel);
}
@@ -25,7 +25,7 @@ class CustomValidator extends BaseValidator {
return this.joi
.string()
.min(fieldOptions.minLength || 0)
- .max(fieldOptions.maxLength || 1000)
+ .max(fieldOptions.maxLength || 10000)
.allow(...allowArgs)
.label(fieldLabel);
}
diff --git a/shared/helpers/filterFunctions.js b/shared/helpers/filterFunctions.js
new file mode 100644
index 000000000..2a5c977f0
--- /dev/null
+++ b/shared/helpers/filterFunctions.js
@@ -0,0 +1,151 @@
+const objectPath = require('object-path');
+const _ = require('lodash');
+
+const _isEmpty = value => {
+ switch (typeof value) {
+ case 'object':
+ return _.isEmpty(value);
+ case 'number':
+ return isNaN(value);
+ case 'string':
+ return value.length === 0;
+ case 'undefined':
+ return true;
+ default:
+ return false;
+ }
+};
+
+const isEmpty = (object, path) => {
+ const value = objectPath.get(object, path);
+ return _isEmpty(value);
+};
+
+const _isEqualTo = (value, targetValue) => {
+ switch (typeof value) {
+ case 'object':
+ return _.isEqual(value, targetValue);
+ case 'number':
+ return value === targetValue;
+ case 'string':
+ if (typeof targetValue === 'string') {
+ return value.trim().toLowerCase() === targetValue.trim().toLowerCase();
+ } else {
+ return value.trim().toLowerCase() == targetValue;
+ }
+ case 'undefined':
+ default:
+ return false;
+ }
+};
+
+const isEqualTo = (object, path, targetValue) => {
+ const value = objectPath.get(object, path);
+ return _isEqualTo(value, targetValue);
+};
+
+const _contains = (value, targetValue) => {
+ if (typeof value === 'string') {
+ if (typeof targetValue === 'string') {
+ return (
+ value
+ .trim()
+ .toLowerCase()
+ .indexOf(targetValue.trim().toLowerCase()) !== -1
+ );
+ }
+ return false;
+ }
+
+ if (Array.isArray(value)) {
+ return value.indexOf(targetValue) !== -1;
+ }
+
+ return false;
+};
+
+const contains = (object, path, targetValue) => {
+ const value = objectPath.get(object, path);
+ return _contains(value, targetValue);
+};
+
+const _containsOneOf = (value, targetValue) => {
+ if (!Array.isArray(targetValue)) return false;
+
+ for (let item of targetValue) {
+ if (_contains(value, item)) return true;
+ }
+
+ return false;
+};
+
+const containsOneOf = (object, path, targetValue) => {
+ const value = objectPath.get(object, path);
+ return _containsOneOf(value, targetValue);
+};
+
+const _isGte = (value, targetValue) => {
+ let numValue = parseInt(value);
+ if (Array.isArray(value) || typeof value === 'string') {
+ numValue = value.length;
+ }
+ const numTarget = parseInt(targetValue);
+
+ if (isNaN(numValue) || isNaN(numTarget)) return false;
+
+ return numValue >= numTarget;
+};
+
+const isGte = (object, path, targetValue) => {
+ const value = objectPath.get(object, path);
+ return _isGte(value, targetValue);
+};
+
+const _isLte = (value, targetValue) => {
+ let numValue = parseInt(value);
+ if (Array.isArray(value) || typeof value === 'string') {
+ numValue = value.length;
+ }
+ const numTarget = parseInt(targetValue);
+
+ if (isNaN(numValue) || isNaN(numTarget)) return false;
+
+ return numValue <= numTarget;
+};
+
+const isLte = (object, path, targetValue) => {
+ const value = objectPath.get(object, path);
+ return _isLte(value, targetValue);
+};
+
+const _isOneOf = (value, targetValue) => {
+ if (!Array.isArray(targetValue)) return false;
+
+ for (let item of targetValue) {
+ if (_isEqualTo(value, item)) return true;
+ }
+
+ return false;
+};
+
+const isOneOf = (object, path, targetValue) => {
+ const value = objectPath.get(object, path);
+ return _isOneOf(value, targetValue);
+};
+
+module.exports = {
+ isEmpty,
+ isEqualTo,
+ isGte,
+ isLte,
+ isOneOf,
+ contains,
+ containsOneOf,
+ _isEmpty,
+ _isEqualTo,
+ _isGte,
+ _isLte,
+ _isOneOf,
+ _contains,
+ _containsOneOf
+};
diff --git a/shared/helpers/filterHelpers.js b/shared/helpers/filterHelpers.js
new file mode 100644
index 000000000..c58e15bea
--- /dev/null
+++ b/shared/helpers/filterHelpers.js
@@ -0,0 +1,100 @@
+const _FilterTypes = require('../constants/filter-types');
+const FilterFunctions = require('./filterFunctions');
+
+const FilterTypes = _FilterTypes.filterTypes;
+
+const buildFiltersArray = filters => {
+ return filters.map((filter = {}) => {
+ switch (filter.type) {
+ case FilterTypes.NOT_EMPTY.id: {
+ return item => {
+ return !FilterFunctions.isEmpty(item, filter.path);
+ };
+ }
+ case FilterTypes.IS_EMPTY.id: {
+ return item => {
+ return FilterFunctions.isEmpty(item, filter.path);
+ };
+ }
+ case FilterTypes.EQUALS.id: {
+ return item => {
+ return FilterFunctions.isEqualTo(item, filter.path, filter.value);
+ };
+ }
+ case FilterTypes.NOT_EQUALS.id: {
+ return item => {
+ return !FilterFunctions.isEqualTo(item, filter.path, filter.value);
+ };
+ }
+ case FilterTypes.ONE_OF.id: {
+ return item => {
+ return FilterFunctions.isOneOf(item, filter.path, filter.value);
+ };
+ }
+ case FilterTypes.NOT_ONE_OF.id: {
+ return item => {
+ return !FilterFunctions.isOneOf(item, filter.path, filter.value);
+ };
+ }
+ case FilterTypes.CONTAINS.id: {
+ return item => {
+ return FilterFunctions.contains(item, filter.path, filter.value);
+ };
+ }
+ case FilterTypes.NOT_CONTAINS.id: {
+ return item => {
+ return !FilterFunctions.contains(item, filter.path, filter.value);
+ };
+ }
+ case FilterTypes.CONTAINS_ONE_OF.id: {
+ return item => {
+ return FilterFunctions.containsOneOf(item, filter.path, filter.value);
+ };
+ }
+ case FilterTypes.NOT_CONTAINS_ONE_OF.id: {
+ return item => {
+ return !FilterFunctions.containsOneOf(item, filter.path, filter.value);
+ };
+ }
+ case FilterTypes.LESS_THAN.id: {
+ return item => {
+ return !FilterFunctions.isGte(item, filter.path, filter.value);
+ };
+ }
+ case FilterTypes.NOT_LESS_THAN.id: {
+ return item => {
+ return FilterFunctions.isGte(item, filter.path, filter.value);
+ };
+ }
+ case FilterTypes.MORE_THAN.id: {
+ return item => {
+ return !FilterFunctions.isLte(item, filter.path, filter.value);
+ };
+ }
+ case FilterTypes.NOT_MORE_THAN.id: {
+ return item => {
+ return FilterFunctions.isLte(item, filter.path, filter.value);
+ };
+ }
+ case FilterTypes.BOOLEAN_TRUE.id:
+ case FilterTypes.BOOLEAN_FALSE.id:
+ default:
+ return () => true;
+ }
+ });
+};
+
+const applyFilters = (items, filters) => {
+ const filtersArray = buildFiltersArray(filters);
+
+ return items.filter(item => {
+ for (let filter of filtersArray) {
+ if (!filter(item)) return false;
+ }
+ return true;
+ });
+};
+
+module.exports = {
+ applyFilters
+};
diff --git a/shared/index.js b/shared/index.js
index 84e83b68f..8bc146a20 100644
--- a/shared/index.js
+++ b/shared/index.js
@@ -9,11 +9,16 @@ module.exports = {
Industries: require('./constants/industries'),
Languages: require('./constants/languages'),
Misc: require('./constants/misc'),
+ FieldTypes: require('./constants/field-types'),
+ FilterTypes: require('./constants/filter-types'),
+ FilterValues: require('./constants/filter-values'),
+ FilterHelpers: require('./helpers/filterHelpers'),
RegistrationFields: require('./constants/registration-fields'),
RegistrationStatuses: require('./constants/registration-statuses'),
RegistrationValidator: require('./helpers/registrationValidator'),
Roles: require('./constants/roles'),
Skills: require('./constants/skills'),
+ SelectOptions: require('./constants/select-options'),
Themes: require('./constants/themes'),
Universities: require('./constants/universities'),
Utils: require('./helpers/utils')
diff --git a/shared/package-lock.json b/shared/package-lock.json
index eb0651471..6f3ad5873 100644
--- a/shared/package-lock.json
+++ b/shared/package-lock.json
@@ -1,5 +1,1004 @@
{
"name": "@hackjunction/shared",
"version": "1.1.45",
- "lockfileVersion": 1
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "ansi-colors": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
+ "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==",
+ "dev": true
+ },
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "cliui": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz",
+ "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^2.1.1",
+ "strip-ansi": "^4.0.0",
+ "wrap-ansi": "^2.0.0"
+ }
+ },
+ "code-point-at": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+ "dev": true
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true
+ },
+ "cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
+ "requires": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true
+ },
+ "define-properties": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
+ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+ "dev": true,
+ "requires": {
+ "object-keys": "^1.0.12"
+ }
+ },
+ "diff": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+ "dev": true
+ },
+ "emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
+ },
+ "end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dev": true,
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
+ "es-abstract": {
+ "version": "1.14.2",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.14.2.tgz",
+ "integrity": "sha512-DgoQmbpFNOofkjJtKwr87Ma5EW4Dc8fWhD0R+ndq7Oc456ivUfGOOP6oAZTTKl5/CcNMP+EN+e3/iUzgE0veZg==",
+ "dev": true,
+ "requires": {
+ "es-to-primitive": "^1.2.0",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.0",
+ "is-callable": "^1.1.4",
+ "is-regex": "^1.0.4",
+ "object-inspect": "^1.6.0",
+ "object-keys": "^1.1.1",
+ "string.prototype.trimleft": "^2.0.0",
+ "string.prototype.trimright": "^2.0.0"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz",
+ "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "execa": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^6.0.0",
+ "get-stream": "^4.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ }
+ },
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
+ "flat": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz",
+ "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==",
+ "dev": true,
+ "requires": {
+ "is-buffer": "~2.0.3"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true
+ },
+ "get-stream": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+ "dev": true,
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "growl": {
+ "version": "1.10.5",
+ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "has-symbols": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
+ "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
+ "dev": true
+ },
+ "he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "invert-kv": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
+ "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
+ "dev": true
+ },
+ "is-buffer": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
+ "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==",
+ "dev": true
+ },
+ "is-callable": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
+ "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
+ "dev": true
+ },
+ "is-date-object": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
+ "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+ "dev": true
+ },
+ "is-regex": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
+ "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.1"
+ }
+ },
+ "is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
+ "dev": true
+ },
+ "is-symbol": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz",
+ "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.0"
+ }
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "lcid": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
+ "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
+ "dev": true,
+ "requires": {
+ "invert-kv": "^2.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "lodash": {
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
+ },
+ "log-symbols": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
+ "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.0.1"
+ }
+ },
+ "map-age-cleaner": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
+ "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
+ "dev": true,
+ "requires": {
+ "p-defer": "^1.0.0"
+ }
+ },
+ "mem": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
+ "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
+ "dev": true,
+ "requires": {
+ "map-age-cleaner": "^0.1.1",
+ "mimic-fn": "^2.0.0",
+ "p-is-promise": "^2.0.0"
+ }
+ },
+ "mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "mocha": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.2.0.tgz",
+ "integrity": "sha512-qwfFgY+7EKAAUAdv7VYMZQknI7YJSGesxHyhn6qD52DV8UcSZs5XwCifcZGMVIE4a5fbmhvbotxC0DLQ0oKohQ==",
+ "dev": true,
+ "requires": {
+ "ansi-colors": "3.2.3",
+ "browser-stdout": "1.3.1",
+ "debug": "3.2.6",
+ "diff": "3.5.0",
+ "escape-string-regexp": "1.0.5",
+ "find-up": "3.0.0",
+ "glob": "7.1.3",
+ "growl": "1.10.5",
+ "he": "1.2.0",
+ "js-yaml": "3.13.1",
+ "log-symbols": "2.2.0",
+ "minimatch": "3.0.4",
+ "mkdirp": "0.5.1",
+ "ms": "2.1.1",
+ "node-environment-flags": "1.0.5",
+ "object.assign": "4.1.0",
+ "strip-json-comments": "2.0.1",
+ "supports-color": "6.0.0",
+ "which": "1.3.1",
+ "wide-align": "1.1.3",
+ "yargs": "13.2.2",
+ "yargs-parser": "13.0.0",
+ "yargs-unparser": "1.5.0"
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+ "dev": true
+ },
+ "nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true
+ },
+ "node-environment-flags": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz",
+ "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==",
+ "dev": true,
+ "requires": {
+ "object.getownpropertydescriptors": "^2.0.3",
+ "semver": "^5.7.0"
+ }
+ },
+ "npm-run-path": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+ "dev": true,
+ "requires": {
+ "path-key": "^2.0.0"
+ }
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+ "dev": true
+ },
+ "object-inspect": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz",
+ "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==",
+ "dev": true
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true
+ },
+ "object-path": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.4.tgz",
+ "integrity": "sha1-NwrnUvvzfePqcKhhwju6iRVpGUk="
+ },
+ "object.assign": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+ "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.2",
+ "function-bind": "^1.1.1",
+ "has-symbols": "^1.0.0",
+ "object-keys": "^1.0.11"
+ }
+ },
+ "object.getownpropertydescriptors": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
+ "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.2",
+ "es-abstract": "^1.5.1"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "os-locale": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
+ "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
+ "dev": true,
+ "requires": {
+ "execa": "^1.0.0",
+ "lcid": "^2.0.0",
+ "mem": "^4.0.0"
+ }
+ },
+ "p-defer": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
+ "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=",
+ "dev": true
+ },
+ "p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
+ "dev": true
+ },
+ "p-is-promise": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz",
+ "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==",
+ "dev": true
+ },
+ "p-limit": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
+ "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true
+ },
+ "path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+ "dev": true
+ },
+ "pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "dev": true,
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true
+ },
+ "require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+ "dev": true
+ },
+ "shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^1.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+ "dev": true
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+ "dev": true
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "string.prototype.trimleft": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz",
+ "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3",
+ "function-bind": "^1.1.1"
+ }
+ },
+ "string.prototype.trimright": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz",
+ "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3",
+ "function-bind": "^1.1.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "strip-eof": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
+ "dev": true
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
+ "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+ "dev": true
+ },
+ "wide-align": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+ "dev": true,
+ "requires": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "wrap-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+ "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+ "dev": true,
+ "requires": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "dev": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ }
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true
+ },
+ "y18n": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+ "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+ "dev": true
+ },
+ "yargs": {
+ "version": "13.2.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz",
+ "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==",
+ "dev": true,
+ "requires": {
+ "cliui": "^4.0.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "os-locale": "^3.1.0",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^13.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "yargs-parser": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz",
+ "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ },
+ "yargs-unparser": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz",
+ "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==",
+ "dev": true,
+ "requires": {
+ "flat": "^4.1.0",
+ "lodash": "^4.17.11",
+ "yargs": "^12.0.5"
+ },
+ "dependencies": {
+ "get-caller-file": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
+ "dev": true
+ },
+ "require-main-filename": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
+ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
+ "dev": true
+ },
+ "yargs": {
+ "version": "12.0.5",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz",
+ "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==",
+ "dev": true,
+ "requires": {
+ "cliui": "^4.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^1.0.1",
+ "os-locale": "^3.0.0",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^1.0.1",
+ "set-blocking": "^2.0.0",
+ "string-width": "^2.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^3.2.1 || ^4.0.0",
+ "yargs-parser": "^11.1.1"
+ }
+ },
+ "yargs-parser": {
+ "version": "11.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz",
+ "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ }
+ }
+ }
+ }
}
diff --git a/shared/package.json b/shared/package.json
index c25a3ff65..fef368a3f 100644
--- a/shared/package.json
+++ b/shared/package.json
@@ -4,10 +4,18 @@
"description": "",
"main": "index.js",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "mocha",
+ "precommit": "npm run test"
},
"author": "Juuso Lappalainen",
"license": "ISC",
+ "dependencies": {
+ "lodash": "^4.17.15",
+ "object-path": "^0.11.4"
+ },
+ "devDependencies": {
+ "mocha": "^6.2.0"
+ },
"engines": {
"node": "12.3.1",
"npm": "6.9.0"
diff --git a/shared/test/filterFunctions.test.js b/shared/test/filterFunctions.test.js
new file mode 100644
index 000000000..64b6fa3c1
--- /dev/null
+++ b/shared/test/filterFunctions.test.js
@@ -0,0 +1,439 @@
+/* eslint-disable */
+
+const assert = require('assert');
+const filterFunctions = require('../helpers/filterFunctions.js');
+
+const EMPTY_STRING = '';
+const STRING = 'Hello world';
+const EMPTY_ARRAY = [];
+const ARRAY = ['one', 'two', 'three'];
+const EMPTY_OBJECT = {};
+const OBJECT = {
+ foo: 'bar'
+};
+const ZERO_NUMBER = 0;
+const NUMBER = 777;
+const STRING_ZERO_NUMBER = '0';
+const STRING_NUMBER = '777';
+
+const testObject = {
+ emptyString: EMPTY_STRING,
+ string: STRING,
+ stringPadded: STRING + ' ',
+ emptyObject: EMPTY_OBJECT,
+ object: OBJECT,
+ emptyArray: EMPTY_ARRAY,
+ array: ARRAY,
+ zeroNumber: ZERO_NUMBER,
+ stringZeroNumber: STRING_ZERO_NUMBER,
+ number: NUMBER,
+ stringNumber: STRING_NUMBER,
+ stringNumberPadded: STRING_NUMBER + ' '
+};
+
+const nestedTestObject = {
+ nested: testObject
+};
+
+describe('Filter functions', function() {
+ describe('isEmpty', function() {
+ it('should return true when value is empty string', function() {
+ const value = filterFunctions.isEmpty(testObject, 'emptyString');
+ assert.equal(value, true);
+ });
+
+ it('should return true when value is nested empty string', function() {
+ const value = filterFunctions.isEmpty(nestedTestObject, 'nested.emptyString');
+ assert.equal(value, true);
+ });
+
+ it('should return true when value is empty array', function() {
+ const value = filterFunctions.isEmpty(testObject, 'emptyArray');
+ assert.equal(value, true);
+ });
+
+ it('should return true when value is nested empty array', function() {
+ const value = filterFunctions.isEmpty(nestedTestObject, 'nested.emptyArray');
+ assert.equal(value, true);
+ });
+
+ it('should return true when value is empty object', function() {
+ const value = filterFunctions.isEmpty(testObject, 'emptyObject');
+ assert.equal(value, true);
+ });
+
+ it('should return true when value is nested empty object', function() {
+ const value = filterFunctions.isEmpty(nestedTestObject, 'nested.emptyObject');
+ assert.equal(value, true);
+ });
+
+ it('should return true when value is undefined', function() {
+ const value = filterFunctions.isEmpty(testObject, 'nonExistingPath');
+ assert.equal(value, true);
+ });
+
+ it('should return true when value is nested undefined', function() {
+ const value = filterFunctions.isEmpty(nestedTestObject, 'nested.nonExistingPath');
+ assert.equal(value, true);
+ });
+
+ it('should return false when value is non-empty string', function() {
+ const value = filterFunctions.isEmpty(testObject, 'string');
+ assert.equal(value, false);
+ });
+
+ it('should return false when value is nested non-empty string', function() {
+ const value = filterFunctions.isEmpty(nestedTestObject, 'nested.string');
+ assert.equal(value, false);
+ });
+
+ it('should return false when value is non-empty array', function() {
+ const value = filterFunctions.isEmpty(testObject, 'array');
+ assert.equal(value, false);
+ });
+
+ it('should return false when value is nested non-empty array', function() {
+ const value = filterFunctions.isEmpty(nestedTestObject, 'nested.array');
+ assert.equal(value, false);
+ });
+
+ it('should return false when value is non-empty object', function() {
+ const value = filterFunctions.isEmpty(testObject, 'object');
+ assert.equal(value, false);
+ });
+
+ it('should return false when value is nested non-empty object', function() {
+ const value = filterFunctions.isEmpty(nestedTestObject, 'nested.object');
+ assert.equal(value, false);
+ });
+
+ it('should return false when value is zero', function() {
+ const value = filterFunctions.isEmpty(testObject, 'zeroNumber');
+ assert.equal(value, false);
+ });
+
+ it('should return false when value is nested zero', function() {
+ const value = filterFunctions.isEmpty(nestedTestObject, 'nested.zeroNumber');
+ assert.equal(value, false);
+ });
+
+ it('should return false when value is non-zero number', function() {
+ const value = filterFunctions.isEmpty(testObject, 'number');
+ assert.equal(value, false);
+ });
+
+ it('should return false when value is nested non-zero number', function() {
+ const value = filterFunctions.isEmpty(nestedTestObject, 'nested.number');
+ assert.equal(value, false);
+ });
+ });
+
+ describe('isEqualTo', function() {
+ describe('strings', function() {
+ it('should return true when comparing two empty strings', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'emptyString', '');
+ assert.equal(value, true);
+ });
+
+ it('should return true when comparing two matching strings', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'string', STRING);
+ assert.equal(value, true);
+ });
+ it('should return true when comparing two matching strings with extra whitespace', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'stringPadded', ' ' + STRING);
+ assert.equal(value, true);
+ });
+ it('should return false when comparing two non-matching strings', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'string', 'different string');
+ assert.equal(value, false);
+ });
+ it('should return false when comparing empty string to non-empty string', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'emptyString', STRING);
+ assert.equal(value, false);
+ });
+ it('should return false when comparing non-empty string to arbitrary number', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'string', NUMBER);
+ assert.equal(value, false);
+ });
+ });
+
+ describe('numbers', function() {
+ it('should return true when comparing two zeros', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'zeroNumber', ZERO_NUMBER);
+ assert.equal(value, true);
+ });
+ it('should return true when comparing two arbitrary matching numbers', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'number', NUMBER);
+ assert.equal(value, true);
+ });
+ it('should reutrn false when comparing two arbitrary non-matching numbers', function() {
+ const value = filterFunctions.isEqualTo(
+ testObject,
+ 'number',
+ NUMBER + Math.floor(Math.random() * 1000)
+ );
+ assert.equal(value, false);
+ });
+ it('should return true when comparing string number to matching number', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'stringNumber', NUMBER);
+ assert.equal(value, true);
+ });
+ it('should return false when comparing string number to non-matching number', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'stringNumber', NUMBER + 1);
+ assert.equal(value, false);
+ });
+ it('should return true when comparing string number with whitespace to matching number', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'stringNumberPadded', NUMBER);
+ assert.equal(value, true);
+ });
+ });
+ describe('arrays', function() {
+ it('should return true when comparing two empty arrays', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'emptyArray', []);
+ assert.equal(value, true);
+ });
+ it('should return true when comparing two non-empty arrays', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'array', ARRAY);
+ assert.equal(value, true);
+ });
+ it('should return false when comparing two non-matching arrays', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'array', ARRAY.concat('foo'));
+ assert.equal(value, false);
+ });
+ it('should return false when comparing empty array to empty object', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'emptyArray', {});
+ assert.equal(value, false);
+ });
+ });
+ describe('objects', function() {
+ it('should return true when comparing two empty objects', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'emptyObject', {});
+ assert.equal(value, true);
+ });
+ it('should return true when comparing two matching non-empty objects', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'object', JSON.parse(JSON.stringify(OBJECT)));
+ assert.equal(value, true);
+ });
+ it('should return false when comparing two non-matching objects', function() {
+ const value = filterFunctions.isEqualTo(testObject, 'object', { ...OBJECT, baz: 'bizz' });
+ assert.equal(value, false);
+ });
+ });
+ describe('undefined', function() {
+ it('should return false when comparing undefined to anything, even undefined', function() {
+ const values = [
+ filterFunctions.isEqualTo(testObject, 'some.undefined.path', []),
+ filterFunctions.isEqualTo(testObject, 'some.undefined.path', {}),
+ filterFunctions.isEqualTo(testObject, 'some.undefined.path', OBJECT),
+ filterFunctions.isEqualTo(testObject, 'some.undefined.path', NUMBER),
+ filterFunctions.isEqualTo(testObject, 'some.undefined.path', ZERO_NUMBER),
+ filterFunctions.isEqualTo(testObject, 'some.undefined.path', STRING),
+ filterFunctions.isEqualTo(testObject, 'some.undefined.path', STRING_ZERO_NUMBER),
+ filterFunctions.isEqualTo(testObject, 'some.undefined.path')
+ ];
+ assert.equal(values.indexOf(true), -1);
+ });
+ });
+ });
+ describe('contains', function() {
+ describe('strings', function() {
+ it('should return true when a string value contains the substring, case-insensitive', function() {
+ const values = [
+ filterFunctions.contains(testObject, 'string', 'hello'),
+ filterFunctions.contains(testObject, 'string', 'HeLLo')
+ ];
+
+ assert.equal(values.indexOf(false), -1);
+ });
+ it('should return true when comparing a string value to an empty string', function() {
+ const value = filterFunctions.contains(testObject, 'string', '');
+ assert.equal(value, true);
+ });
+ it('should return false when comparing a string value to a non-matching string', function() {
+ const value = filterFunctions.contains(testObject, 'string', 'something not matching');
+ assert.equal(value, false);
+ });
+ it('should return false when comparing a string value to a number', function() {
+ const value = filterFunctions.contains(testObject, 'string', NUMBER);
+ assert.equal(value, false);
+ });
+ });
+ describe('arrays', function() {
+ it('should return true when the source array contains the target value', function() {
+ const values = [
+ filterFunctions.contains(testObject, 'array', 'one'),
+ filterFunctions.contains(testObject, 'array', 'two')
+ ];
+ assert.equal(values.indexOf(false), -1);
+ });
+ it('should return false when the source array does not contain the target value', function() {
+ const value = filterFunctions.contains(testObject, 'array', 'four');
+ assert.equal(value, false);
+ });
+ it('should return false when the source array is empty', function() {
+ const values = [
+ filterFunctions.contains(testObject, 'emptyArray', ''),
+ filterFunctions.contains(testObject, 'emptyArray', 'two')
+ ];
+ assert.equal(values.indexOf(true), -1);
+ });
+ });
+ describe('others', function() {
+ it('should return false when the source array is not an array', function() {
+ const values = [
+ filterFunctions.contains(testObject, 'object', ''),
+ filterFunctions.contains(testObject, 'object', 'foo'),
+ filterFunctions.contains(testObject, 'number', ''),
+ filterFunctions.contains(testObject, 'number', NUMBER),
+ filterFunctions.contains(testObject, 'something.undefined', ''),
+ filterFunctions.contains(testObject, 'something.undefined', undefined)
+ ];
+ assert.equal(values.indexOf(true), -1);
+ });
+ });
+ });
+ describe('isGte / isLte', function() {
+ describe('numbers', function() {
+ it('isGte should return true when the source number is equal', function() {
+ const value = filterFunctions.isGte(testObject, 'number', NUMBER);
+ assert.equal(value, true);
+ });
+
+ it('isGte should return true when the source number is larger than the target number', function() {
+ const value = filterFunctions.isGte(testObject, 'number', NUMBER - 1);
+ assert.equal(value, true);
+ });
+
+ it('isGte should return false when the source number is smaller than the target number', function() {
+ const value = filterFunctions.isGte(testObject, 'number', NUMBER + 1);
+ assert.equal(value, false);
+ });
+
+ it('isLte should return true when the source number is equal', function() {
+ const value = filterFunctions.isLte(testObject, 'number', NUMBER);
+ assert.equal(value, true);
+ });
+
+ it('isLte should return true when the source number is smaller than the target number', function() {
+ const value = filterFunctions.isLte(testObject, 'number', NUMBER + 1);
+ assert.equal(value, true);
+ });
+
+ it('isLte should return false when the source number is larger than the target number', function() {
+ const value = filterFunctions.isLte(testObject, 'number', NUMBER - 1);
+ assert.equal(value, false);
+ });
+ });
+ describe('arrays', function() {
+ it('isGte / isLte should work with arrays, and compare their length to the target number', function() {
+ const passingValues = [
+ filterFunctions.isGte(testObject, 'array', ARRAY.length),
+ filterFunctions.isGte(testObject, 'array', ARRAY.length - 1),
+ filterFunctions.isGte(testObject, 'emptyArray', 0),
+ filterFunctions.isGte(testObject, 'array', '1'),
+ filterFunctions.isLte(testObject, 'array', ARRAY.length),
+ filterFunctions.isLte(testObject, 'array', ARRAY.length + 1),
+ filterFunctions.isLte(testObject, 'emptyArray', 0),
+ filterFunctions.isLte(testObject, 'emptyArray', 3),
+ filterFunctions.isLte(testObject, 'emptyArray', '0'),
+ filterFunctions.isLte(testObject, 'emptyArray', '7')
+ ];
+
+ const failingValues = [
+ filterFunctions.isLte(testObject, 'array', ARRAY.length - 1),
+ filterFunctions.isGte(testObject, 'array', ARRAY.length + 1),
+ filterFunctions.isGte(testObject, 'array', 10),
+ filterFunctions.isGte(testObject, 'array', '10')
+ ];
+
+ assert.equal(passingValues.indexOf(false), -1);
+ assert.equal(failingValues.indexOf(true), -1);
+ });
+ });
+ describe('strings', function() {
+ it('isGte / isLte should work with strings, and compare their length to the target number', function() {
+ const passingValues = [
+ filterFunctions.isGte(testObject, 'string', STRING.length - 1),
+ filterFunctions.isGte(testObject, 'string', STRING.length),
+ filterFunctions.isGte(testObject, 'emptyString', 0),
+ filterFunctions.isLte(testObject, 'string', STRING.length + 1),
+ filterFunctions.isLte(testObject, 'string', STRING.length),
+ filterFunctions.isLte(testObject, 'emptyString', 0)
+ ];
+ const failingValues = [
+ filterFunctions.isGte(testObject, 'string', STRING.length + 1),
+ filterFunctions.isLte(testObject, 'string', STRING.length - 1)
+ ];
+
+ assert.equal(passingValues.indexOf(false), -1);
+ assert.equal(failingValues.indexOf(true), -1);
+ });
+ });
+ describe('others', function() {
+ it('both should return false when the source number is an object or undefined', function() {
+ const values = [
+ filterFunctions.isGte(testObject, 'object', 0),
+ filterFunctions.isGte(testObject, 'object', 1),
+ filterFunctions.isGte(testObject, 'something.undefined', 0)
+ ];
+ assert.equal(values.indexOf(true), -1);
+ });
+ it('both should return false when the target value is not a number', function() {
+ const values = [
+ filterFunctions.isGte(testObject, 'string', OBJECT),
+ filterFunctions.isLte(testObject, 'string', OBJECT),
+ filterFunctions.isGte(testObject, 'string', ARRAY),
+ filterFunctions.isLte(testObject, 'string', ARRAY),
+ filterFunctions.isGte(testObject, 'string'),
+ filterFunctions.isLte(testObject, 'string'),
+ filterFunctions.isGte(testObject, 'emptyString'),
+ filterFunctions.isLte(testObject, 'emptyString')
+ ];
+
+ assert.equal(values.indexOf(true), -1);
+ });
+ });
+ });
+ describe('isOneOf', function() {
+ it('should return false when target value is empty array', function() {
+ const value = filterFunctions._isOneOf('something', []);
+ assert.equal(value, false);
+ });
+
+ it('should return false when target value does not include the value', function() {
+ const value = filterFunctions._isOneOf('something', ['other', 'third thing']);
+ assert.equal(value, false);
+ });
+
+ it('should return true when target value includes the value', function() {
+ const value = filterFunctions._isOneOf('something', ['something', 'other']);
+ assert.equal(value, true);
+ });
+ });
+ describe('containsOneOf', function() {
+ it('should return false when value is not an array', function() {
+ const value = filterFunctions._containsOneOf('foobar', ['some', 'values']);
+ assert.equal(value, false);
+ });
+ it('should return false when target value is not an array', function() {
+ const value = filterFunctions._containsOneOf(['foobar'], 'hello');
+ assert.equal(value, false);
+ });
+ it('should return false when target value is empty array', function() {
+ const value = filterFunctions._containsOneOf(['foobar'], []);
+ assert.equal(value, false);
+ });
+ it('should return false when target value is not contained in value', function() {
+ const value = filterFunctions._containsOneOf(['foo'], ['bar']);
+ assert.equal(value, false);
+ });
+ it('should return true when target value is contained in value', function() {
+ const value1 = filterFunctions._containsOneOf(['foo', 'buzz'], ['foo', 'bar']);
+ assert.equal(value1, true);
+ const value2 = filterFunctions._containsOneOf(['fizz'], ['fizz']);
+ assert.equal(value2, true);
+ const value3 = filterFunctions._containsOneOf(['fizz'], ['fizz', 'buzz']);
+ assert.equal(value3, true);
+ });
+ });
+});