From 97101d908a333aaa1206fa0b45d4e32d7323c5db Mon Sep 17 00:00:00 2001 From: Juuso Lappalainen Date: Thu, 3 Oct 2019 18:31:35 +0300 Subject: [PATCH 1/5] Bulk edit modal improvements --- backend/modules/email-task/controller.js | 4 + backend/modules/email-task/routes.js | 1 + backend/modules/registration/controller.js | 16 +- frontend/package-lock.json | 92 +++-- .../components/generic/ConfirmDialog/index.js | 32 ++ .../src/components/generic/Modal/index.js | 7 +- .../generic/UserListItem/OrganiserListItem.js | 16 + .../components/generic/UserListItem/index.js | 24 ++ .../src/components/inputs/TextInput/index.js | 19 +- .../modals/BulkEditRegistrationModal/index.js | 227 ++++++------- .../components/modals/BulkEmailModal/index.js | 320 ++++++++++++++++++ .../modals/OrganiserSelectModal/index.js | 56 +++ .../components/tables/AttendeeTable/index.js | 12 +- shared/constants/filter-types.js | 6 +- shared/constants/registration-fields.js | 41 ++- 15 files changed, 703 insertions(+), 170 deletions(-) create mode 100644 frontend/src/components/generic/ConfirmDialog/index.js create mode 100644 frontend/src/components/generic/UserListItem/OrganiserListItem.js create mode 100644 frontend/src/components/generic/UserListItem/index.js create mode 100644 frontend/src/components/modals/BulkEmailModal/index.js create mode 100644 frontend/src/components/modals/OrganiserSelectModal/index.js diff --git a/backend/modules/email-task/controller.js b/backend/modules/email-task/controller.js index 582d78b1f..90225af30 100644 --- a/backend/modules/email-task/controller.js +++ b/backend/modules/email-task/controller.js @@ -77,10 +77,14 @@ controller.createTravelGrantRejectedTask = async (registration, deliverNow = fal controller.createGenericTask = async (userId, eventId, uniqueId, msgParams, deliverNow = false) => { if (!uniqueId) { + console.log('GENERATING UNIQUE ID'); uniqueId = shortid.generate(); } + console.log('CREATING TASK'); const task = await controller.createTask(userId, eventId, 'generic_' + uniqueId, msgParams); + console.log('CREATED TASK'); if (task && deliverNow) { + console.log('DELIVERING NOW', task); return controller.deliverEmailTask(task); } return task; diff --git a/backend/modules/email-task/routes.js b/backend/modules/email-task/routes.js index 5a8fd15df..55e98c110 100644 --- a/backend/modules/email-task/routes.js +++ b/backend/modules/email-task/routes.js @@ -15,6 +15,7 @@ const sendPreviewEmail = asyncHandler(async (req, res) => { }); const sendBulkEmail = asyncHandler(async (req, res) => { + console.log('BODY', req.body); await EmailTaskController.sendBulkEmail(req.body.recipients, req.body.params, req.event, req.body.uniqueId); return res.status(200).json({}); }); diff --git a/backend/modules/registration/controller.js b/backend/modules/registration/controller.js index 65ae0606b..cc88f6da9 100644 --- a/backend/modules/registration/controller.js +++ b/backend/modules/registration/controller.js @@ -1,5 +1,6 @@ const _ = require('lodash'); const Promise = require('bluebird'); +const mongoose = require('mongoose'); const { RegistrationStatuses, RegistrationFields, FieldTypes } = require('@hackjunction/shared'); const Registration = require('./model'); const { NotFoundError, ForbiddenError } = require('../../common/errors/errors'); @@ -177,13 +178,16 @@ controller.rejectPendingTravelGrants = eventId => { }; controller.getFullRegistration = (eventId, registrationId) => { - return Registration.findById(registrationId).then(registration => { - if (!registration || registration.event.toString() !== eventId) { - throw new NotFoundError(`Registration with id ${registrationId} does not exist`); - } + const query = mongoose.Types.ObjectId.isValid(registrationId) ? { _id: registrationId } : { user: registrationId }; + return Registration.findOne(query) + .and({ event: eventId }) + .then(registration => { + if (!registration) { + throw new NotFoundError(`Registration with id ${registrationId} does not exist`); + } - return registration; - }); + return registration; + }); }; controller.editRegistration = (registrationId, event, data, user) => { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 22e4dd8f6..01a953ad0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1057,16 +1057,6 @@ "requires": { "lodash": "^4.17.15", "object-path": "^0.11.4" - }, - "dependencies": { - "lodash": { - "version": "4.17.15", - "bundled": true - }, - "object-path": { - "version": "0.11.4", - "bundled": true - } } }, "@hapi/address": { @@ -3630,7 +3620,8 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true }, "aproba": { "version": "1.2.0", @@ -3651,12 +3642,14 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3671,17 +3664,20 @@ "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=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3798,7 +3794,8 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "optional": true }, "ini": { "version": "1.3.5", @@ -3810,6 +3807,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3824,6 +3822,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3831,12 +3830,14 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true }, "minipass": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3855,6 +3856,7 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3935,7 +3937,8 @@ "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=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3947,6 +3950,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, "requires": { "wrappy": "1" } @@ -4032,7 +4036,8 @@ "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==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4068,6 +4073,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4087,6 +4093,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4130,12 +4137,14 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "optional": true } } }, @@ -8906,7 +8915,8 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true }, "aproba": { "version": "1.2.0", @@ -8927,12 +8937,14 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8947,17 +8959,20 @@ "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=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -9074,7 +9089,8 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "optional": true }, "ini": { "version": "1.3.5", @@ -9086,6 +9102,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9100,6 +9117,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -9107,12 +9125,14 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true }, "minipass": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -9131,6 +9151,7 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, "requires": { "minimist": "0.0.8" } @@ -9211,7 +9232,8 @@ "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=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true }, "object-assign": { "version": "4.1.1", @@ -9223,6 +9245,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, "requires": { "wrappy": "1" } @@ -9308,7 +9331,8 @@ "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==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -9344,6 +9368,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9363,6 +9388,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -9406,12 +9432,14 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "optional": true } } } diff --git a/frontend/src/components/generic/ConfirmDialog/index.js b/frontend/src/components/generic/ConfirmDialog/index.js new file mode 100644 index 000000000..9713fbb0c --- /dev/null +++ b/frontend/src/components/generic/ConfirmDialog/index.js @@ -0,0 +1,32 @@ +import React, { useCallback } from 'react'; + +import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@material-ui/core'; + +const ConfirmDialog = ({ open, onClose, onCancel, onOk, title, message, cancelText = 'Cancel', okText = 'OK' }) => { + const handleCancel = useCallback(() => { + onClose(); + onCancel(); + }, [onClose, onCancel]); + + const handleOk = useCallback(() => { + onClose(); + onOk(); + }, [onClose, onOk]); + + return ( + + {title} + + {message} + + + + + + + ); +}; + +export default ConfirmDialog; diff --git a/frontend/src/components/generic/Modal/index.js b/frontend/src/components/generic/Modal/index.js index 6116e989e..95decddf4 100644 --- a/frontend/src/components/generic/Modal/index.js +++ b/frontend/src/components/generic/Modal/index.js @@ -32,7 +32,7 @@ const useStyles = makeStyles(theme => ({ }, header: { padding: theme.spacing(3), - textAlign: 'left' + textAlign: 'center' }, inner: { padding: '1rem', @@ -41,7 +41,7 @@ const useStyles = makeStyles(theme => ({ } })); -const GenericModal = ({ title, isOpen, handleClose, size, children }) => { +const GenericModal = ({ title, isOpen, handleClose, size, children, footer = null }) => { const classes = useStyles(); return ( { > {title && ( - {title} + {title} )} {children} + {footer} ); }; diff --git a/frontend/src/components/generic/UserListItem/OrganiserListItem.js b/frontend/src/components/generic/UserListItem/OrganiserListItem.js new file mode 100644 index 000000000..001780a82 --- /dev/null +++ b/frontend/src/components/generic/UserListItem/OrganiserListItem.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import * as OrganiserSelectors from 'redux/organiser/selectors'; +import UserListItem from './index'; + +const OrganiserListItem = ({ userId, organisersMap = {} }) => { + console.log('USER ID', userId); + console.log('ORGANISER MAP', organisersMap); + return ; +}; + +const mapState = state => ({ + organisersMap: OrganiserSelectors.organisersMap(state) +}); + +export default connect(mapState)(OrganiserListItem); diff --git a/frontend/src/components/generic/UserListItem/index.js b/frontend/src/components/generic/UserListItem/index.js new file mode 100644 index 000000000..0e97244af --- /dev/null +++ b/frontend/src/components/generic/UserListItem/index.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { ListItem, ListItemAvatar, ListItemText, Avatar } from '@material-ui/core'; + +const UserListItem = ({ user, selectable = false, selected = false, onSelect = () => {} }) => { + const userName = user ? `${user.firstName} ${user.lastName}` : ''; + const userEmail = user ? user.email : ''; + + return ( + + {user ? ( + + + + + + + ) : ( + + )} + + ); +}; + +export default UserListItem; diff --git a/frontend/src/components/inputs/TextInput/index.js b/frontend/src/components/inputs/TextInput/index.js index 0664411a9..897fc8004 100644 --- a/frontend/src/components/inputs/TextInput/index.js +++ b/frontend/src/components/inputs/TextInput/index.js @@ -2,7 +2,7 @@ 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, formatValue, formatOnChange }) => { +const TextInput = ({ label, helperText, value = '', onChange = () => {}, error, disabled, rawOnChange = false, type = 'text', textarea = false, formatValue, formatOnChange }) => { const handleChange = useCallback( e => { if (rawOnChange) { @@ -13,12 +13,25 @@ const TextInput = ({ label, helperText, value = '', onChange = () => {}, error, onChange(val); } }, - [onChange, rawOnChange] + [onChange, rawOnChange, formatOnChange] ); const formattedValue = formatValue ? formatValue(value) : value; - return ; + return( + + ); }; export default TextInput; diff --git a/frontend/src/components/modals/BulkEditRegistrationModal/index.js b/frontend/src/components/modals/BulkEditRegistrationModal/index.js index 33672bf8a..302e81575 100644 --- a/frontend/src/components/modals/BulkEditRegistrationModal/index.js +++ b/frontend/src/components/modals/BulkEditRegistrationModal/index.js @@ -3,75 +3,58 @@ import { connect } from 'react-redux'; import Modal from 'components/generic/Modal'; import { withSnackbar } from 'notistack'; -import { Typography, ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Button } from '@material-ui/core'; +import { + Box, + Typography, + ExpansionPanel, + ExpansionPanelSummary, + ExpansionPanelDetails, + Button, + ListItem, + ListItemText, + Divider, + Paper +} from '@material-ui/core'; import Rating from '@material-ui/lab/Rating'; import PageWrapper from 'components/PageWrapper'; import CenteredContainer from 'components/generic/CenteredContainer'; import PageHeader from 'components/generic/PageHeader'; -import UserSelectModal from 'components/modals/UserSelectModal'; +import OrganiserSelectModal from 'components/modals/OrganiserSelectModal'; +import OrganiserListItem from 'components/generic/UserListItem/OrganiserListItem'; +import EventTagsSelect from 'components/FormComponents/EventTagsSelect'; +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 { useFormField } from 'hooks/formHooks'; -const BulkEditRegistrationModal = ({ registrationIds = [], onClose, organisers }) => { +const BulkEditRegistrationModal = ({ visible, registrationIds = [], onClose, organisers, event }) => { const [loading, setLoading] = useState(false); + const [organiserModal, setOrganiserModal] = useState(false); const rating = useFormField(0); const assignedTo = useFormField(null); - // const [error, setError] = useState(false); - // const [registration, setRegistration] = useState(); - // const { slug } = event; + const tags = useFormField([]); + const status = useFormField('pending'); - // 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 [expandedIds, setExpandedIds] = useState([]); - // const participantName = useMemo(() => { - // if (!registration) return ''; - // const { firstName, lastName } = registration.answers; - // return `${firstName} ${lastName}`; - // }, [registration]); + const isExpanded = panel => { + return expandedIds.indexOf(panel) !== -1; + }; - // const participantSubheading = useMemo(() => { - // if (!registration) return ''; - // return registration.answers.countryOfResidence; - // }, [registration]); + const toggleExpanded = panel => { + if (isExpanded(panel)) { + setExpandedIds(expandedIds.filter(id => id !== panel)); + } else { + setExpandedIds(expandedIds.concat(panel)); + } + }; - // 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] - // ); + if (!registrationIds.length) return null; return ( - + @@ -80,95 +63,105 @@ const BulkEditRegistrationModal = ({ registrationIds = [], onClose, organisers } want to edit - if a panel is left un-expanded, that field will not be edited in the registrations. - + toggleExpanded('rating')}> - Rating + + Rating + {isExpanded('rating') ? ( + + {rating.value ? 'Set rating to ' + rating.value : 'Clear rating'} + + ) : ( + + Leave unchanged + + )} + - + rating.setValue(num)} /> - + toggleExpanded('assignedTo')}> - Assigned to + + Assigned to + {isExpanded('assignedTo') ? ( + + {assignedTo.value ? 'Change assigned to' : 'Clear assigned to'} + + ) : ( + + Leave unchanged + + )} + - ( - - {assignedTo.value} - - {assignedTo && ( - - )} - - )} - onDone={value => assignedTo.setValue(value.userId)} - allowMultiple={false} - userProfiles={organisers} + assignedTo.setValue(value.userId)} + onClear={assignedTo.setValue} /> - Details here + + + + + + - + toggleExpanded('tags')}> - Tags + + Tags + {isExpanded('tags') ? ( + + {tags.value && tags.value.length + ? 'Set tags to ' + tags.value.join(', ') + : 'Clear tags'} + + ) : ( + + Leave unchanged + + )} + - Details here + - + toggleExpanded('status')}> - Status + + Status + {isExpanded('status') ? ( + + Set status to {status.value} + + ) : ( + + Leave unchanged + + )} + - Details here + + + + - {/* - - ( - - {renderAssignedTo()} - - Change - - {assignedTo && ( - setAssignedTo(null)}> - Clear - - )} - - )} - onDone={handleAssignedChange} - allowMultiple={false} - userProfiles={organisers} - /> - - - - - - - - - - {renderPreview()} - + + + + + + {/* + + setBody(e.target.value)} + autosize={{ + minRows: 10, + maxRows: 20 + }} + > + The content of your email + + + setMessageId(e.target.value)} + size="large" + placeholder="something-you-will-remember" + /> + + If you want, you can enter a unique message id here. Messages with the same id will only be sent + once to a given participant - this is useful if you want to avoid sending out the same message to a + participant who has already received it earlier. + + + + setCtaText(e.target.value)} + size="large" + placeholder="Click this button" + /> + + If your want a Call to Action button in your email, enter the text for the button here. + + + + setCtaLink(e.target.value)} + size="large" + placeholder="https://..." + /> + Enter the link where your Call to Action button should lead + + + Test email + + + + + Send to {registrationIds.length} recipients + + */} + + + + ); +}; + +const mapState = state => ({ + idToken: AuthSelectors.getIdToken(state), + user: UserSelectors.userProfile(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 + )(BulkEmailModal) +); diff --git a/frontend/src/components/modals/OrganiserSelectModal/index.js b/frontend/src/components/modals/OrganiserSelectModal/index.js new file mode 100644 index 000000000..7dd3ef2ae --- /dev/null +++ b/frontend/src/components/modals/OrganiserSelectModal/index.js @@ -0,0 +1,56 @@ +import React, { useState, useCallback } from 'react'; +import { connect } from 'react-redux'; + +import { Box, List, Button } from '@material-ui/core'; + +import Modal from 'components/generic/Modal'; +import * as OrganiserSelectors from 'redux/organiser/selectors'; + +import UserListItem from 'components/generic/UserListItem'; + +const OrganiserSelectModal = ({ open, onClose, onClear, onSelect, organisers }) => { + const [selected, setSelected] = useState(); + const handleClear = useCallback(() => { + onClose(); + onClear(); + }, [onClose, onClear]); + const handleSubmit = useCallback(() => { + onClose(); + onSelect(selected); + }, [selected, onClose, onSelect]); + return ( + + + + + + } + > + + {organisers.map(organiser => ( + setSelected(organiser)} + selected={selected && selected.userId === organiser.userId} + key={organisers.userId} + user={organiser} + /> + ))} + + + ); +}; + +const mapState = state => ({ + organisers: OrganiserSelectors.organisers(state) +}); +export default connect(mapState)(OrganiserSelectModal); diff --git a/frontend/src/components/tables/AttendeeTable/index.js b/frontend/src/components/tables/AttendeeTable/index.js index 0ddc413a2..1ad2728ec 100644 --- a/frontend/src/components/tables/AttendeeTable/index.js +++ b/frontend/src/components/tables/AttendeeTable/index.js @@ -12,6 +12,7 @@ import MaterialTable from 'components/generic/MaterialTable'; import * as OrganiserSelectors from 'redux/organiser/selectors'; import EditRegistrationModal from 'components/modals/EditRegistrationModal'; import BulkEditRegistrationModal from 'components/modals/BulkEditRegistrationModal'; +import BulkEmailModal from 'components/modals/BulkEmailModal'; const AttendeeTable = ({ organiserProfilesMap, @@ -45,8 +46,8 @@ const AttendeeTable = ({ title={title} isLoading={loading} data={attendees} - onRowClick={(e, row) => setEditing(row._id)} - onSelectionChange={rows => setSelected(rows.map(r => r._id))} + onRowClick={(e, row) => setEditing(row.user)} + onSelectionChange={rows => setSelected(rows.map(r => r.user))} actions={[ { icon: forwardRef((props, ref) => ), @@ -159,11 +160,8 @@ const AttendeeTable = ({ return ( - diff --git a/shared/constants/filter-types.js b/shared/constants/filter-types.js index 30f04dbc3..ebf79b573 100644 --- a/shared/constants/filter-types.js +++ b/shared/constants/filter-types.js @@ -109,6 +109,7 @@ const numberFilterTypes = [ filterTypes.NOT_MORE_THAN.id ]; +const objectFilterTypes = [filterTypes.IS_EMPTY.id, filterTypes.NOT_EMPTY.id]; const booleanFilterTypes = [filterTypes.BOOLEAN_TRUE.id, filterTypes.BOOLEAN_FALSE.id]; const STRING = 'STRING'; @@ -116,6 +117,7 @@ const ARRAY = 'ARRAY'; const NUMBER = 'NUMBER'; const BOOLEAN = 'BOOLEAN'; const DATE = 'DATE'; +const OBJECT = 'OBJECT'; module.exports = { filterTypes, @@ -124,11 +126,13 @@ module.exports = { ARRAY: arrayFilterTypes, NUMBER: numberFilterTypes, BOOLEAN: booleanFilterTypes, + OBJECT: objectFilterTypes, DATE: [] }, STRING, ARRAY, NUMBER, BOOLEAN, - DATE + DATE, + OBJECT }; diff --git a/shared/constants/registration-fields.js b/shared/constants/registration-fields.js index 7df80a2c8..209f02db6 100644 --- a/shared/constants/registration-fields.js +++ b/shared/constants/registration-fields.js @@ -1112,7 +1112,46 @@ function buildFiltersArray() { return res.concat(filters); }, []); - return baseFilters.concat(answerFilters); + const extraFilters = [ + { + label: 'Terminal', + path: 'answers.terminal', + type: FilterTypes.OBJECT, + valueType: FilterValues.BOOLEAN + }, + { + label: 'Terminal > Motivation', + path: 'answers.terminal.motivation', + type: FilterTypes.STRING, + valueType: FilterValues.STRING + }, + { + label: 'Terminal > Most fascinating project', + path: 'answers.terminal.mostFascinatingProject', + type: FilterTypes.STRING, + valueType: FilterValues.STRING + }, + { + label: 'Terminal > Ideal work environment', + path: 'answers.terminal.idealWorkEnvironment', + type: FilterTypes.STRING, + valueType: FilterValues.STRING + }, + { + label: 'Terminal > What makes you awesome', + path: 'answers.terminal.whatMakesYouAwesome', + type: FilterTypes.STRING, + valueType: FilterValues.STRING + }, + { + label: 'Terminal > Accomodation', + path: 'answers.terminal.accomodation', + type: FilterTypes.STRING, + valueType: FilterValues.STRING + } + ]; + + return baseFilters.concat(answerFilters).concat(extraFilters); } const Helpers = { From 9f31f22add58548dc7feb7bcc98cd4cd645721ec Mon Sep 17 00:00:00 2001 From: Juuso Lappalainen Date: Thu, 3 Oct 2019 19:10:23 +0300 Subject: [PATCH 2/5] Fix bulk edit modal --- backend/modules/registration/controller.js | 2 +- .../components/generic/ConfirmDialog/index.js | 11 ++- .../generic/UserListItem/OrganiserListItem.js | 2 - .../modals/BulkEditRegistrationModal/index.js | 90 +++++++++++++++---- frontend/src/hooks/formHooks.js | 4 +- 5 files changed, 88 insertions(+), 21 deletions(-) diff --git a/backend/modules/registration/controller.js b/backend/modules/registration/controller.js index cc88f6da9..81b62a8c6 100644 --- a/backend/modules/registration/controller.js +++ b/backend/modules/registration/controller.js @@ -138,7 +138,7 @@ controller.bulkEditRegistrations = (eventId, registrationIds, edits) => { return Registration.updateMany( { event: eventId, - _id: { + user: { $in: registrationIds } }, diff --git a/frontend/src/components/generic/ConfirmDialog/index.js b/frontend/src/components/generic/ConfirmDialog/index.js index 9713fbb0c..4742c195e 100644 --- a/frontend/src/components/generic/ConfirmDialog/index.js +++ b/frontend/src/components/generic/ConfirmDialog/index.js @@ -2,7 +2,16 @@ import React, { useCallback } from 'react'; import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@material-ui/core'; -const ConfirmDialog = ({ open, onClose, onCancel, onOk, title, message, cancelText = 'Cancel', okText = 'OK' }) => { +const ConfirmDialog = ({ + open, + onClose = () => {}, + onCancel = () => {}, + onOk = () => {}, + title, + message, + cancelText = 'Cancel', + okText = 'OK' +}) => { const handleCancel = useCallback(() => { onClose(); onCancel(); diff --git a/frontend/src/components/generic/UserListItem/OrganiserListItem.js b/frontend/src/components/generic/UserListItem/OrganiserListItem.js index 001780a82..51e2e49f9 100644 --- a/frontend/src/components/generic/UserListItem/OrganiserListItem.js +++ b/frontend/src/components/generic/UserListItem/OrganiserListItem.js @@ -4,8 +4,6 @@ import * as OrganiserSelectors from 'redux/organiser/selectors'; import UserListItem from './index'; const OrganiserListItem = ({ userId, organisersMap = {} }) => { - console.log('USER ID', userId); - console.log('ORGANISER MAP', organisersMap); return ; }; diff --git a/frontend/src/components/modals/BulkEditRegistrationModal/index.js b/frontend/src/components/modals/BulkEditRegistrationModal/index.js index 302e81575..282075cdd 100644 --- a/frontend/src/components/modals/BulkEditRegistrationModal/index.js +++ b/frontend/src/components/modals/BulkEditRegistrationModal/index.js @@ -9,11 +9,7 @@ import { ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, - Button, - ListItem, - ListItemText, - Divider, - Paper + Button } from '@material-ui/core'; import Rating from '@material-ui/lab/Rating'; import PageWrapper from 'components/PageWrapper'; @@ -23,25 +19,38 @@ import OrganiserSelectModal from 'components/modals/OrganiserSelectModal'; import OrganiserListItem from 'components/generic/UserListItem/OrganiserListItem'; import EventTagsSelect from 'components/FormComponents/EventTagsSelect'; import RegistrationStatusSelect from 'components/FormComponents/RegistrationStatusSelect'; +import ConfirmDialog from 'components/generic/ConfirmDialog'; import * as AuthSelectors from 'redux/auth/selectors'; import * as OrganiserSelectors from 'redux/organiser/selectors'; import * as OrganiserActions from 'redux/organiser/actions'; import { useFormField } from 'hooks/formHooks'; -const BulkEditRegistrationModal = ({ visible, registrationIds = [], onClose, organisers, event }) => { +const BulkEditRegistrationModal = ({ + visible, + registrationIds = [], + onClose, + onSubmit, + organisers, + event, + enqueueSnackbar +}) => { const [loading, setLoading] = useState(false); const [organiserModal, setOrganiserModal] = useState(false); - const rating = useFormField(0); + const [confirmDialog, setConfirmDialog] = useState(false); + const rating = useFormField(null); const assignedTo = useFormField(null); const tags = useFormField([]); const status = useFormField('pending'); const [expandedIds, setExpandedIds] = useState([]); - const isExpanded = panel => { - return expandedIds.indexOf(panel) !== -1; - }; + const isExpanded = useCallback( + panel => { + return expandedIds.indexOf(panel) !== -1; + }, + [expandedIds] + ); const toggleExpanded = panel => { if (isExpanded(panel)) { @@ -51,11 +60,57 @@ const BulkEditRegistrationModal = ({ visible, registrationIds = [], onClose, org } }; - if (!registrationIds.length) return null; + const reset = useCallback(() => { + rating.reset(); + assignedTo.reset(); + tags.reset(); + status.reset(); + setLoading(false); + setExpandedIds([]); + }, [rating, assignedTo, tags, status]); + + const handleClose = useCallback(() => { + reset(); + onClose(); + }, [reset, onClose]); + const getEdits = useCallback(() => { + const edits = {}; + if (isExpanded('rating')) edits.rating = rating.value; + if (isExpanded('assignedTo')) edits.assignedTo = assignedTo.value; + if (isExpanded('tags')) edits.tags = tags.value; + if (isExpanded('status')) edits.status = status.value; + return edits; + }, [rating, assignedTo, tags, status, isExpanded]); + + const handleSubmit = useCallback(() => { + setLoading(true); + const edits = getEdits(); + + onSubmit(registrationIds, edits, event.slug) + .then(() => { + enqueueSnackbar(`Edited ${registrationIds.length} registrations`, { variant: 'success' }); + }) + .catch(err => { + enqueueSnackbar('Something went wrong', { variant: 'error' }); + }) + .finally(() => { + setLoading(false); + handleClose(); + }); + }, [onSubmit, handleClose, getEdits, event.slug, registrationIds, enqueueSnackbar]); + + if (!registrationIds.length) return null; return ( - + + setConfirmDialog(false)} + onOk={handleSubmit} + /> @@ -155,7 +210,12 @@ const BulkEditRegistrationModal = ({ visible, registrationIds = [], onClose, org - + + + + + + ); }; @@ -85,7 +87,9 @@ const mapDispatch = dispatch => ({ updateRegistrations: slug => dispatch(OrganiserActions.updateRegistrationsForEvent(slug)) }); -export default connect( - mapState, - mapDispatch -)(SearchAttendeesPage); +export default withSnackbar( + connect( + mapState, + mapDispatch + )(SearchAttendeesPage) +); From 48124e1eee574bc8106217bb94a54fee321cb64e Mon Sep 17 00:00:00 2001 From: Juuso Lappalainen Date: Fri, 4 Oct 2019 00:07:41 +0300 Subject: [PATCH 5/5] Update React version --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index f0c161f52..2e2583eb5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,7 +25,7 @@ "node-sass": "^4.11.0", "notistack": "^0.9.2", "object-path": "^0.11.4", - "react": "^16.8.1", + "react": "^16.9.0", "react-animate-height": "^2.0.15", "react-app-rewired": "^2.1.3", "react-dom": "^16.8.1",