From 9d5739b3960026a8149fc36628eafa1b53bc42f2 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 12 Oct 2017 15:27:57 -0600 Subject: [PATCH 01/40] better support for email verification --- errors.js | 49 +++++++++++++++++---------- routes/api/users/index.js | 48 ++++++++++++++++++-------- services/limit.js | 47 ++++++++++++++++++++++++++ services/passport.js | 2 +- services/users.js | 50 +++++++--------------------- test/server/routes/api/auth/index.js | 4 +-- test/server/services/users.js | 12 ++++--- 7 files changed, 134 insertions(+), 78 deletions(-) create mode 100644 services/limit.js diff --git a/errors.js b/errors.js index ab1667e07..93ebf34b9 100644 --- a/errors.js +++ b/errors.js @@ -97,7 +97,8 @@ class ErrAssetCommentingClosed extends APIError { class ErrAuthentication extends APIError { constructor(message = null) { super('authentication error occured', { - status: 401 + status: 401, + translation_key: 'AUTHENTICATION' }, { message }); @@ -195,31 +196,45 @@ const ErrAssetURLAlreadyExists = new APIError('Asset URL already exists, cannot status: 409 }); +// ErrNotVerified is returned when a user tries to login with valid credentials +// but their email address is not yet verified. +const ErrNotVerified = new APIError('User does not have a verified email address', { + translation_key: 'EMAIL_NOT_VERIFIED', + status: 401, +}); + +const ErrMaxRateLimit = new APIError('Rate limit exeeded', { + translation_key: 'RATE_LIMIT_EXCEEDED', + status: 429, +}); + module.exports = { - ExtendableError, APIError, ErrAlreadyExists, - ErrPasswordTooShort, - ErrSettingsNotInit, + ErrAssetCommentingClosed, + ErrAssetURLAlreadyExists, + ErrAuthentication, + ErrCommentTooShort, + ErrContainsProfanity, + ErrEditWindowHasEnded, + ErrEmailTaken, + ErrInstallLock, + ErrInvalidAssetURL, + ErrLoginAttemptMaximumExceeded, + ErrMaxRateLimit, ErrMissingEmail, ErrMissingPassword, ErrMissingToken, - ErrEmailTaken, - ErrSpecialChars, ErrMissingUsername, - ErrContainsProfanity, - ErrUsernameTaken, - ErrAssetCommentingClosed, - ErrNotFound, - ErrInvalidAssetURL, - ErrAuthentication, ErrNotAuthorized, + ErrNotFound, + ErrNotVerified, + ErrPasswordTooShort, ErrPermissionUpdateUsername, ErrSameUsernameProvided, ErrSettingsInit, - ErrInstallLock, - ErrLoginAttemptMaximumExceeded, - ErrEditWindowHasEnded, - ErrCommentTooShort, - ErrAssetURLAlreadyExists + ErrSettingsNotInit, + ErrSpecialChars, + ErrUsernameTaken, + ExtendableError, }; diff --git a/routes/api/users/index.js b/routes/api/users/index.js index 189d0ea3e..d076288e6 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -5,6 +5,7 @@ const mailer = require('../../../services/mailer'); const errors = require('../../../errors'); const authorization = require('../../../middleware/authorization'); const i18n = require('../../../services/i18n'); +const Limit = require('../../../services/limit'); const { ROOT_URL } = require('../../../config'); @@ -109,16 +110,15 @@ router.post('/:user_id/email', authorization.needed('ADMIN', 'MODERATOR'), async /** * SendEmailConfirmation sends a confirmation email to the user. - * @param {ExpressApp} app the instance of the express app * @param {String} userID the id for the user to send the email to * @param {String} email the email for the user to send the email to */ -const SendEmailConfirmation = async (app, userID, email, referer) => { - let token = await UsersService.createEmailConfirmToken(userID, email, referer); +const SendEmailConfirmation = async (user, email, referer) => { + let token = await UsersService.createEmailConfirmToken(user, email, referer); return mailer.sendSimple({ - template: 'email-confirm', // needed to know which template to render! - locals: { // specifies the template locals. + template: 'email-confirm', + locals: { token, rootURL: ROOT_URL, email @@ -138,7 +138,7 @@ router.post('/', async (req, res, next) => { // Send an email confirmation. The Front end will know about the // requireEmailConfirmation as it's included in the settings get endpoint. - await SendEmailConfirmation(req.app, user.id, email, redirectUri); + await SendEmailConfirmation(user, email, redirectUri); res.status(201).json(user); } catch (e) { @@ -166,25 +166,45 @@ router.post('/:user_id/actions', authorization.needed(), async (req, res, next) } }); +// This will allow 1 try every minute. +const resendRateLimiter = new Limit('/api/v1/users/resend-verify', 1, '1m'); + // trigger an email confirmation re-send by a new user router.post('/resend-verify', async (req, res, next) => { - const {email} = req.body; const redirectUri = req.header('X-Pym-Url') || req.header('Referer'); + let {email = ''} = req.body; - if (!email) { + // Clean up and validate the email. + email = email.toLowerCase().trim(); + if (email.length < 5) { return next(errors.ErrMissingEmail); } + // Check if we're past the rate limit, if we are, stop now. Otherwise, record + // this as an attempt to send a verification email. try { + const tries = await resendRateLimiter.get(email); + if (tries > 0) { + throw errors.ErrMaxRateLimit; + } - // find user by email. - // if the local profile is verified, return an error code? - // send a 204 after the email is re-sent - await SendEmailConfirmation(req.app, null, email, redirectUri); + await resendRateLimiter.test(email); + } catch (err) { + return next(err); + } + + try { + const user = await UsersService.findLocalUser(email); + if (!user) { + throw errors.ErrNotFound; + } + + await SendEmailConfirmation(user, email, redirectUri); res.status(204).end(); } catch (e) { - return next(e); + console.trace(e); + res.status(204).end(); } }); @@ -208,7 +228,7 @@ router.post('/:user_id/email/confirm', authorization.needed('ADMIN', 'MODERATOR' } // Send the email to the first local profile that was found. - await SendEmailConfirmation(req.app, user.id, localProfile.id); + await SendEmailConfirmation(user, localProfile.id); res.status(204).end(); } catch (e) { diff --git a/services/limit.js b/services/limit.js new file mode 100644 index 000000000..7acc78f0c --- /dev/null +++ b/services/limit.js @@ -0,0 +1,47 @@ +const ms = require('ms'); +const errors = require('../errors'); +const {createClientFactory} = require('./redis'); +const client = createClientFactory(); + +class Limit { + constructor(prefix, max, duration) { + this.ttl = ms(duration) / 1000; + this.prefix = prefix; + this.max = max; + } + + key(value) { + return `limit[${this.prefix}][${value}]`; + } + + async get(value) { + const key = this.key(value); + + return client().get(key); + } + + async test(value) { + const key = this.key(value); + + const [[, tries], [, expiry]] = await client() + .multi() + .incr(key) + .expire(key, this.ttl) + .exec(); + + // if this is new or has no expiry + if (tries === 1 || expiry === -1) { + + // then expire it after the timeout + client().expire(key, this.ttl); + } + + if (tries > this.max) { + throw errors.ErrMaxRateLimit; + } + + return tries; + } +} + +module.exports = Limit; diff --git a/services/passport.js b/services/passport.js index 83635fa85..0920c1c39 100644 --- a/services/passport.js +++ b/services/passport.js @@ -145,7 +145,7 @@ async function ValidateUserLogin(loginProfile, user, done) { // If the profile doesn't have a metadata field, or it does not have a // confirmed_at field, or that field is null, then send them back. if (!profile.metadata || !profile.metadata.confirmed_at || profile.metadata.confirmed_at === null) { - return done(new errors.ErrAuthentication(loginProfile.id)); + return done(errors.ErrNotVerified); } } diff --git a/services/users.js b/services/users.js index 78708b40a..4e9d52901 100644 --- a/services/users.js +++ b/services/users.js @@ -17,7 +17,7 @@ const UserModel = require('../models/user'); const USER_STATUS = require('../models/enum/user_status'); const USER_ROLES = require('../models/enum/user_roles'); -const RECAPTCHA_WINDOW_SECONDS = 60 * 10; // 10 minutes. +const RECAPTCHA_WINDOW = '10m'; // 10 minutes. const RECAPTCHA_INCORRECT_TRIGGER = 5; // after 3 incorrect attempts, recaptcha will be required. const ActionsService = require('./actions'); @@ -34,9 +34,8 @@ const PASSWORD_RESET_JWT_SUBJECT = 'password_reset'; const SALT_ROUNDS = 10; // Create a redis client to use for authentication. -const {createClientFactory} = require('./redis'); - -const client = createClientFactory(); +const Limit = require('./limit'); +const loginRateLimiter = new Limit('loginAttempts', RECAPTCHA_INCORRECT_TRIGGER, RECAPTCHA_WINDOW); // UsersService is the interface for the application to interact with the // UserModel through. @@ -70,23 +69,14 @@ module.exports = class UsersService { * where future login attempts must be made with the recaptcha flag. */ static async recordLoginAttempt(email) { - const rdskey = `la[${email.toLowerCase().trim()}]`; + try { + await loginRateLimiter.test(email.toLowerCase().trim()); + } catch (err) { + if (err === errors.ErrMaxRateLimit) { + throw errors.ErrLoginAttemptMaximumExceeded; + } - const replies = await client() - .multi() - .incr(rdskey) - .expire(rdskey, RECAPTCHA_WINDOW_SECONDS) - .exec(); - - // if this is new or has no expiry - if (replies[0] === 1 || replies[1] === -1) { - - // then expire it after the timeout - client().expire(rdskey, RECAPTCHA_WINDOW_SECONDS); - } - - if (replies[0] >= RECAPTCHA_INCORRECT_TRIGGER) { - throw errors.ErrLoginAttemptMaximumExceeded; + throw err; } } @@ -97,9 +87,7 @@ module.exports = class UsersService { * errors.ErrLoginAttemptMaximumExceeded */ static async checkLoginAttempts(email) { - const rdskey = `la[${email.toLowerCase().trim()}]`; - - const attempts = await client().get(rdskey); + const attempts = await loginRateLimiter.get(email.toLowerCase().trim()); if (!attempts) { return; } @@ -717,7 +705,7 @@ module.exports = class UsersService { * @param {String} email The email that we are needing to get confirmed. * @return {Promise} */ - static async createEmailConfirmToken(userID = null, email, referer = ROOT_URL) { + static async createEmailConfirmToken(user, email, referer = ROOT_URL) { if (!email || typeof email !== 'string') { throw new Error('email is required when creating a JWT for resetting passord'); } @@ -731,20 +719,6 @@ module.exports = class UsersService { subject: EMAIL_CONFIRM_JWT_SUBJECT }; - let user; - if (!userID) { - - // If there is no userID, we're coming from the endpoint where a new user - // is re-requesting a confirmation email and we don't know the userID. - user = await UserModel.findOne({profiles: {$elemMatch: {id: email, provider: 'local'}}}); - } else { - user = await UsersService.findById(userID); - } - - if (!user) { - throw errors.ErrNotFound; - } - // Get the profile representing the local account. let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local'); diff --git a/test/server/routes/api/auth/index.js b/test/server/routes/api/auth/index.js index 2f3c4116a..463e27711 100644 --- a/test/server/routes/api/auth/index.js +++ b/test/server/routes/api/auth/index.js @@ -74,10 +74,8 @@ describe('/api/v1/auth/local', () => { .catch((err) => { expect(err).to.have.status(401); err.response.body.should.have.property('error'); - err.response.body.error.should.have.property('metadata'); - err.response.body.error.metadata.should.have.property('message', 'maria@gmail.com'); - return UsersService.createEmailConfirmToken(mockUser.id, mockUser.profiles[0].id); + return UsersService.createEmailConfirmToken(mockUser, mockUser.profiles[0].id); }) .then(UsersService.verifyEmailConfirmation) .then(() => { diff --git a/test/server/services/users.js b/test/server/services/users.js index 251618187..09a8396c4 100644 --- a/test/server/services/users.js +++ b/test/server/services/users.js @@ -88,17 +88,19 @@ describe('services.UsersService', () => { describe('#createEmailConfirmToken', () => { it('should create a token for a valid user', async () => { - const token = await UsersService.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id); + const token = await UsersService.createEmailConfirmToken(mockUsers[0], mockUsers[0].profiles[0].id); expect(token).to.not.be.null; }); it('should not create a token for a user already verified', async () => { - const token = await UsersService.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id); + const token = await UsersService.createEmailConfirmToken(mockUsers[0], mockUsers[0].profiles[0].id); expect(token).to.not.be.null; await UsersService.verifyEmailConfirmation(token); - return expect(UsersService.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id)).to.eventually.be.rejected; + const user = await UsersService.findById(mockUsers[0].id); + + return expect(UsersService.createEmailConfirmToken(user, mockUsers[0].profiles[0].id)).to.eventually.be.rejected; }); }); @@ -106,7 +108,7 @@ describe('services.UsersService', () => { describe('#verifyEmailConfirmation', () => { it('should correctly validate a valid token', async () => { - const token = await UsersService.createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id); + const token = await UsersService.createEmailConfirmToken(mockUsers[0], mockUsers[0].profiles[0].id); expect(token).to.not.be.null; return expect(UsersService.verifyEmailConfirmation(token)).to.eventually.not.be.rejected; @@ -122,7 +124,7 @@ describe('services.UsersService', () => { it('should update the user model when verification is complete', () => { return UsersService - .createEmailConfirmToken(mockUsers[0].id, mockUsers[0].profiles[0].id) + .createEmailConfirmToken(mockUsers[0], mockUsers[0].profiles[0].id) .then((token) => { expect(token).to.not.be.null; From 910acdd52bcfa21d9d11aaf418857297e2e5653a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 12 Oct 2017 15:49:21 -0600 Subject: [PATCH 02/40] added cli tools for verifying email address --- bin/cli-users | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/bin/cli-users b/bin/cli-users index c6555ab28..89f53a67d 100755 --- a/bin/cli-users +++ b/bin/cli-users @@ -241,13 +241,21 @@ function listUsers() { }); users.forEach((user) => { + let state = user.disabled ? 'Disabled' : 'Enabled'; + const profile = user.profiles.find(({provider}) => provider === 'local'); + if (profile && profile.metadata && profile.metadata.confirmed_at) { + state += ', Verified'; + } else { + state += ', Unverified'; + } + table.push([ user.id, user.username, user.profiles.map((p) => p.provider).join(', '), user.roles.join(', '), user.status, - user.disabled ? 'Disabled' : 'Enabled' + state ]); }); @@ -396,6 +404,23 @@ function enableUser(userID) { }); } +/** + * Verifies an email address for a user. + * + * @param userID the user's id + * @param email the user's email address to be verified + */ +async function verify(userID, email) { + try { + await UsersService.confirmEmail(userID, email); + console.log(`User ${userID} had their email ${email} verified.`); + util.shutdown(); + } catch (err) { + console.error(err); + util.shutdown(1); + } +} + //============================================================================== // Setting up the program command line arguments. //============================================================================== @@ -467,6 +492,11 @@ program .description('enable a given user from logging in') .action(enableUser); +program + .command('verify ') + .description('verifies the given user\'s email address') + .action(verify); + program.parse(process.argv); // If there is no command listed, output help. From d75ae3aca8db5f0fa5cd5387426ef56fc42ad0b7 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Mon, 23 Oct 2017 18:46:41 +0200 Subject: [PATCH 03/40] Revive resend verification feature --- client/coral-embed-stream/src/actions/auth.js | 23 ++-- .../coral-embed-stream/src/constants/auth.js | 1 + .../coral-embed-stream/src/reducers/auth.js | 15 ++- locales/en.yml | 2 + .../client/components/ResendVerification.css | 15 +++ .../client/components/ResendVerification.js | 43 +++++++ .../client/components/SignDialog.js | 13 +- .../client/components/SignInContainer.js | 20 +-- .../client/components/SignInContent.js | 114 ++++++++---------- .../talk-plugin-auth/client/translations.yml | 6 +- 10 files changed, 150 insertions(+), 102 deletions(-) create mode 100644 plugins/talk-plugin-auth/client/components/ResendVerification.css create mode 100644 plugins/talk-plugin-auth/client/components/ResendVerification.js diff --git a/client/coral-embed-stream/src/actions/auth.js b/client/coral-embed-stream/src/actions/auth.js index b96d6f459..22d46a768 100644 --- a/client/coral-embed-stream/src/actions/auth.js +++ b/client/coral-embed-stream/src/actions/auth.js @@ -23,6 +23,10 @@ export const hideSignInDialog = () => (dispatch) => { dispatch({type: actions.HIDE_SIGNIN_DIALOG}); }; +export const resetSignInDialog = () => (dispatch) => { + dispatch({type: actions.HIDE_SIGNIN_DIALOG}); +}; + export const focusSignInDialog = () => ({ type: actions.FOCUS_SIGNIN_DIALOG, }); @@ -94,8 +98,9 @@ export const cleanState = () => ({ // Sign In Actions -const signInRequest = () => ({ - type: actions.FETCH_SIGNIN_REQUEST +const signInRequest = (email) => ({ + type: actions.FETCH_SIGNIN_REQUEST, + email, }); const signInFailure = (error) => ({ @@ -122,7 +127,7 @@ export const handleAuthToken = (token) => (dispatch, _, {storage}) => { export const fetchSignIn = (formData) => { return (dispatch, _, {rest}) => { - dispatch(signInRequest()); + dispatch(signInRequest(formData.email)); return rest('/auth/local', {method: 'POST', body: formData}) .then(({token}) => { @@ -144,8 +149,7 @@ export const fetchSignIn = (formData) => { // invalid credentials dispatch(signInFailure(t('error.email_password'), error.metadata)); } else { - const str = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); - dispatch(signInFailure(str)); + dispatch(signInFailure(error)); } }); }; @@ -349,8 +353,9 @@ const verifyEmailSuccess = () => ({ type: actions.VERIFY_EMAIL_SUCCESS }); -const verifyEmailFailure = () => ({ - type: actions.VERIFY_EMAIL_FAILURE +const verifyEmailFailure = (error) => ({ + type: actions.VERIFY_EMAIL_FAILURE, + error, }); export const requestConfirmEmail = (email) => (dispatch, getState, {rest}) => { @@ -366,8 +371,8 @@ export const requestConfirmEmail = (email) => (dispatch, getState, {rest}) => { }) .catch((error) => { console.error(error); - const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); - dispatch(verifyEmailFailure(errorMessage)); + dispatch(verifyEmailFailure(error)); + throw error; }); }; diff --git a/client/coral-embed-stream/src/constants/auth.js b/client/coral-embed-stream/src/constants/auth.js index 324f75f44..bb2ea6c15 100644 --- a/client/coral-embed-stream/src/constants/auth.js +++ b/client/coral-embed-stream/src/constants/auth.js @@ -53,3 +53,4 @@ export const UPDATE_USERNAME = 'UPDATE_USERNAME'; export const SET_REQUIRE_EMAIL_VERIFICATION = 'SET_REQUIRE_EMAIL_VERIFICATION'; export const SET_REDIRECT_URI = 'SET_REDIRECT_URI'; +export const RESET_SIGNIN_DIALOG = 'RESET_SIGNIN_DIALOG'; diff --git a/client/coral-embed-stream/src/reducers/auth.js b/client/coral-embed-stream/src/reducers/auth.js index 1e18ddffa..679e8d858 100644 --- a/client/coral-embed-stream/src/reducers/auth.js +++ b/client/coral-embed-stream/src/reducers/auth.js @@ -10,7 +10,7 @@ const initialState = { showCreateUsernameDialog: false, checkedInitialLogin: false, view: 'SIGNIN', - error: '', + error: null, passwordRequestSuccess: null, passwordRequestFailure: null, emailVerificationFailure: false, @@ -45,14 +45,15 @@ export default function auth (state = initialState, action) { showSignInDialog: true, signInDialogFocus: true, }; - case actions.HIDE_SIGNIN_DIALOG : + case actions.RESET_SIGNIN_DIALOG: + case actions.HIDE_SIGNIN_DIALOG: return { ...state, isLoading: false, showSignInDialog: false, signInDialogFocus: false, view: 'SIGNIN', - error: '', + error: null, passwordRequestFailure: null, passwordRequestSuccess: null, emailVerificationFailure: false, @@ -74,7 +75,7 @@ export default function auth (state = initialState, action) { return { ...state, showCreateUsernameDialog: false, - error: '', + error: null, }; case actions.CREATE_USERNAME_FAILURE: return { @@ -92,6 +93,7 @@ export default function auth (state = initialState, action) { case actions.FETCH_SIGNIN_REQUEST: return { ...state, + email: action.email, isLoading: true, }; case actions.CHECK_LOGIN_FAILURE: @@ -120,6 +122,7 @@ export default function auth (state = initialState, action) { isLoading: false, error: action.error, user: null, + view: action.error.translation_key === 'EMAIL_NOT_VERIFIED' ? 'RESEND_VERIFICATION' : state.view, }; case actions.FETCH_SIGNUP_FACEBOOK_REQUEST: return { @@ -175,7 +178,7 @@ export default function auth (state = initialState, action) { case actions.VALID_FORM: return { ...state, - error: '', + error: null, }; case actions.FETCH_FORGOT_PASSWORD_SUCCESS: return { @@ -200,7 +203,7 @@ export default function auth (state = initialState, action) { case actions.VERIFY_EMAIL_FAILURE: return { ...state, - emailVerificationFailure: true, + emailVerificationFailure: action.error, emailVerificationLoading: false, }; case actions.VERIFY_EMAIL_REQUEST: diff --git a/locales/en.yml b/locales/en.yml index 2d560fedc..76a434dfc 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -194,8 +194,10 @@ en: NO_SPECIAL_CHARACTERS: "Usernames can contain letters numbers and _ only" PASSWORD_LENGTH: "Password is too short" PROFANITY_ERROR: "Usernames must not contain profanity. Please contact the administrator if you believe this to be in error." + RATE_LIMIT_EXCEEDED: "Rate limit exceeded" USERNAME_IN_USE: "Username already in use" USERNAME_REQUIRED: "Must input a username" + EMAIL_NOT_VERIFIED: "E-mail address not verified" EDIT_WINDOW_ENDED: "You can no longer edit this comment. The time window to do so has expired." EDIT_USERNAME_NOT_AUTHORIZED: "You do not have permission to update your username." SAME_USERNAME_PROVIDED: "You must submit a different username." diff --git a/plugins/talk-plugin-auth/client/components/ResendVerification.css b/plugins/talk-plugin-auth/client/components/ResendVerification.css new file mode 100644 index 000000000..dc59a9ff1 --- /dev/null +++ b/plugins/talk-plugin-auth/client/components/ResendVerification.css @@ -0,0 +1,15 @@ +.header { + margin-bottom: 20px; + text-align: center; + font-size: 1.2em; +} + +.notVerified { + padding: 10px; + margin-bottom: 20px; + border-radius: 2px; + border: solid 1px #dddd00; + background: #FFFF9C; + color: #777700; +} + diff --git a/plugins/talk-plugin-auth/client/components/ResendVerification.js b/plugins/talk-plugin-auth/client/components/ResendVerification.js new file mode 100644 index 000000000..b345577a9 --- /dev/null +++ b/plugins/talk-plugin-auth/client/components/ResendVerification.js @@ -0,0 +1,43 @@ +import React from 'react'; +import {Button, Spinner, Success, Alert} from 'plugin-api/beta/client/components/ui'; +import PropTypes from 'prop-types'; +import styles from './ResendVerification.css'; +import t from 'coral-framework/services/i18n'; + +class ResendVerification extends React.Component { + render() { + const {resendVerification, error, loading, success, email} = this.props; + return ( +
+

+ {t('sign_in.email_verify_cta')} +

+ + {error && + + {error.translation_key ? t(`error.${error.translation_key}`) : error.toString()} + } +
+ {t('error.email_not_verified', email)} +
+
+ + {loading && } + {success && } +
+
+ ); + } +} + +ResendVerification.propTypes = { + resendVerification: PropTypes.bool.isRequired, + error: PropTypes.object, + loading: PropTypes.bool, + success: PropTypes.bool, + email: PropTypes.string.isRequired, +}; + +export default ResendVerification; diff --git a/plugins/talk-plugin-auth/client/components/SignDialog.js b/plugins/talk-plugin-auth/client/components/SignDialog.js index 542f53f98..652148d75 100644 --- a/plugins/talk-plugin-auth/client/components/SignDialog.js +++ b/plugins/talk-plugin-auth/client/components/SignDialog.js @@ -5,13 +5,22 @@ import styles from './styles.css'; import SignInContent from './SignInContent'; import SignUpContent from './SignUpContent'; import ForgotContent from './ForgotContent'; +import ResendVerification from './ResendVerification'; -const SignDialog = ({open, view, hideSignInDialog, ...props}) => ( +const SignDialog = ({open, view, resetSignInDialog, ...props}) => ( - × + {view !== 'SIGNIN' && ×} {view === 'SIGNIN' && } {view === 'SIGNUP' && } {view === 'FORGOT' && } + {view === 'RESEND_VERIFICATION' && + } ); diff --git a/plugins/talk-plugin-auth/client/components/SignInContainer.js b/plugins/talk-plugin-auth/client/components/SignInContainer.js index 011ce45f1..c22ad72c4 100644 --- a/plugins/talk-plugin-auth/client/components/SignInContainer.js +++ b/plugins/talk-plugin-auth/client/components/SignInContainer.js @@ -15,6 +15,7 @@ import { fetchSignUpFacebook, fetchForgotPassword, requestConfirmEmail, + resetSignInDialog, facebookCallback, invalidForm, validForm, @@ -31,7 +32,6 @@ class SignInContainer extends React.Component { password: '', confirmPassword: '' }, - emailToBeResent: '', errors: {}, showErrors: false }; @@ -80,26 +80,15 @@ class SignInContainer extends React.Component { ); }; - handleChangeEmail = (e) => { - const {value} = e.target; - this.setState({emailToBeResent: value}); - }; - - handleResendVerification = (e) => { - e.preventDefault(); + resendVerification = () => { this.props - .requestConfirmEmail( - this.state.emailToBeResent, - ) + .requestConfirmEmail(this.props.auth.email) .then(() => { setTimeout(() => { // allow success UI to be shown for a second, and then close the modal - this.props.hideSignInDialog(); + this.props.resetSignInDialog(); }, 2500); - }) - .catch((err) => { - console.error(err); }); }; @@ -194,6 +183,7 @@ const mapDispatchToProps = (dispatch) => requestConfirmEmail, changeView, hideSignInDialog, + resetSignInDialog, invalidForm, validForm }, diff --git a/plugins/talk-plugin-auth/client/components/SignInContent.js b/plugins/talk-plugin-auth/client/components/SignInContent.js index d1b4caec1..956176b32 100644 --- a/plugins/talk-plugin-auth/client/components/SignInContent.js +++ b/plugins/talk-plugin-auth/client/components/SignInContent.js @@ -1,16 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {Button, TextField, Spinner, Success, Alert} from 'plugin-api/beta/client/components/ui'; +import {Button, TextField, Spinner, Alert} from 'plugin-api/beta/client/components/ui'; import styles from './styles.css'; import t from 'coral-framework/services/i18n'; const SignInContent = ({ handleChange, - handleChangeEmail, - emailToBeResent, - handleResendVerification, - emailVerificationLoading, - emailVerificationSuccess, formData, changeView, handleSignIn, @@ -21,71 +16,56 @@ const SignInContent = ({

- {auth.emailVerificationFailure - ? t('sign_in.email_verify_cta') - : t('sign_in.sign_in_to_join')} + {t('sign_in.sign_in_to_join')}

- {auth.error && {auth.error}} - {auth.emailVerificationFailure - ?
-

{t('sign_in.request_new_verify_email')}

+ {auth.error && + + {auth.error.translation_key ? t(`error.${auth.error.translation_key}`) : auth.error.toString()} + } +
+
+ +
+
+

+ {t('sign_in.or')} +

+
+ - - {emailVerificationLoading && } - {emailVerificationSuccess && } + +
+ {!auth.isLoading + ? + : } +
- :
-
- -
-
-

- {t('sign_in.or')} -

-
-
- - -
- {!auth.isLoading - ? - : } -
- -
} +
changeView('FORGOT')}> @@ -111,12 +91,12 @@ SignInContent.propTypes = { }).isRequired, fetchSignInFacebook: PropTypes.func.isRequired, handleSignIn: PropTypes.func.isRequired, + handleChange: PropTypes.func.isRequired, changeView: PropTypes.func.isRequired, emailVerificationLoading: PropTypes.bool.isRequired, emailVerificationSuccess: PropTypes.bool.isRequired, - handleResendVerification: PropTypes.func.isRequired, - handleChangeEmail: PropTypes.func.isRequired, - emailToBeResent: PropTypes.string.isRequired + resendVerification: PropTypes.func.isRequired, + formData: PropTypes.object, }; export default SignInContent; diff --git a/plugins/talk-plugin-auth/client/translations.yml b/plugins/talk-plugin-auth/client/translations.yml index 189c3d9da..f410ec2d3 100644 --- a/plugins/talk-plugin-auth/client/translations.yml +++ b/plugins/talk-plugin-auth/client/translations.yml @@ -1,7 +1,7 @@ en: sign_in: email_verify_cta: "Please verify your email address." - request_new_verify_email: "Request another email:" + request_new_verify_email: "Request another email" verify_email: "Thank you for creating an account! We sent an email to the address you provided to verify your account." verify_email2: "You must verify your account before engaging with the community." not_you: "Not you?" @@ -45,7 +45,7 @@ en: es: sign_in: email_verify_cta: "Por favor confirme su correo." - request_new_verify_email: "Enviar otro correo:" + request_new_verify_email: "Enviar otro correo" verify_email: "¡Gracias por crear una cuenta! Le enviamos un correo a la direcció\ n que dio para confirmar su cuenta." verify_email2: "Debe confirmarla antes de poder involucrarse en la comunidad." @@ -92,7 +92,7 @@ es: fr: sign_in: email_verify_cta: "Merci de vérifier votre adresse e-mail." - request_new_verify_email: "Demander un nouvel envoi d'e-mail :" + request_new_verify_email: "Demander un nouvel envoi d'e-mail" verify_email: "Merci d'avoir créé un compte ! Nous avons envoyé un courrier électronique à l'adresse que vous avez fournie pour vérifier votre adresse e-mail." verify_email2: "Vous devez vérifier votre adresse e-mail avant de vous engager auprès de la communauté." not_you: "Pas vous ?" From f004573ed2f4ffa6e83a4deb4e365e046623a8ff Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 23 Oct 2017 23:47:05 -0300 Subject: [PATCH 04/40] Replacing Pagination Component --- client/coral-ui/components/Pager.css | 19 ----------- client/coral-ui/components/Pager.js | 43 ------------------------- client/coral-ui/components/Paginate.css | 31 ++++++++++++++++++ client/coral-ui/components/Paginate.js | 34 +++++++++++++++++++ client/coral-ui/index.js | 2 +- 5 files changed, 66 insertions(+), 63 deletions(-) delete mode 100644 client/coral-ui/components/Pager.css delete mode 100644 client/coral-ui/components/Pager.js create mode 100644 client/coral-ui/components/Paginate.css create mode 100644 client/coral-ui/components/Paginate.js diff --git a/client/coral-ui/components/Pager.css b/client/coral-ui/components/Pager.css deleted file mode 100644 index 42559d11d..000000000 --- a/client/coral-ui/components/Pager.css +++ /dev/null @@ -1,19 +0,0 @@ -.pager { - text-align: center; - - li { - display: inline-block; - margin-right: 5px; - color: white; - height: 30px; - text-align: center; - vertical-align: middle; - line-height: 30px; - width: 30px; - } -} - -.current { - background: #696969; - box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12); -} diff --git a/client/coral-ui/components/Pager.js b/client/coral-ui/components/Pager.js deleted file mode 100644 index 8f560593e..000000000 --- a/client/coral-ui/components/Pager.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import styles from './Pager.css'; - -const Rows = (curr, total, onClickHandler) => Array.from(Array(total)).map((e, i) => -
  • onClickHandler(i + 1)}> - {i + 1} -
  • -); - -const Pager = ({totalPages, page, onNewPageHandler}) => ( -
    -
      - { - (totalPages > page && totalPages > 1) ? -
    • onNewPageHandler(page - 1)}> - Prev -
    • - : - null - } - {Rows(page, totalPages, onNewPageHandler)} - { - (page < totalPages && totalPages > 1) ? -
    • onNewPageHandler(page + 1)}> - Next -
    • - : - null - } -
    -
    -); - -Pager.propTypes = { - totalPages: PropTypes.number.isRequired, - page: PropTypes.number.isRequired, -}; - -export default Pager; diff --git a/client/coral-ui/components/Paginate.css b/client/coral-ui/components/Paginate.css new file mode 100644 index 000000000..dbb25b844 --- /dev/null +++ b/client/coral-ui/components/Paginate.css @@ -0,0 +1,31 @@ +.container { + text-align: center; +} + +.page, .previous, .next { + display: inline-block; + margin-right: 5px; + height: 30px; + text-align: center; + vertical-align: middle; + line-height: 30px; + width: 30px; + list-style: none; +} + +.pageLink, .previousLink, .nextLink { + color: #696969; + cursor: default; +} + +.previousLink, .nextLink { + font-size: 1.8em; +} + +.active { + background-color: #696969; + box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12); + a { + color: white; + } +} diff --git a/client/coral-ui/components/Paginate.js b/client/coral-ui/components/Paginate.js new file mode 100644 index 000000000..1d7d55398 --- /dev/null +++ b/client/coral-ui/components/Paginate.js @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactPaginate from 'react-paginate'; +import styles from './Paginate.css'; +import Icon from './Icon'; + +const Paginate = ({pageCount, onPageChange}) => ( + ...
    } + breakClassName={styles.break} + containerClassName={styles.container} + pageClassName={styles.page} + pageLinkClassName={styles.pageLink} + activeClassName={styles.active} + previousLabel={} + previousClassName={styles.previous} + previousLinkClassName={styles.previousLink} + nextLabel={} + nextClassName={styles.next} + nextLinkClassName={styles.nextLink} + /> +); + +Paginate.propTypes = { + pageCount: PropTypes.number.isRequired, + onPageChange: PropTypes.func.isRequired, +}; + +export default Paginate; diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js index c688857a1..a2d8049f7 100644 --- a/client/coral-ui/index.js +++ b/client/coral-ui/index.js @@ -18,7 +18,7 @@ export {default as Item} from './components/Item'; export {default as Card} from './components/Card'; export {default as TextField} from './components/TextField'; export {default as Success} from './components/Success'; -export {default as Pager} from './components/Pager'; +export {default as Paginate} from './components/Paginate'; export {default as Wizard} from './components/Wizard'; export {default as WizardNav} from './components/WizardNav'; export {default as SnackBar} from './components/SnackBar'; From d363c977dca91aadaeef3f769d788418f408e7c6 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 23 Oct 2017 23:47:43 -0300 Subject: [PATCH 05/40] Refactor --- client/coral-admin/src/actions/community.js | 20 ++-- client/coral-admin/src/constants/community.js | 7 +- client/coral-admin/src/reducers/community.js | 26 ++--- .../routes/Community/components/Community.css | 4 + .../routes/Community/components/Community.js | 101 ++++------------- .../src/routes/Community/components/People.js | 53 ++++----- .../src/routes/Community/components/Table.js | 28 ++++- .../routes/Community/containers/Community.js | 48 +-------- .../src/routes/Community/containers/People.js | 102 ++++++++++++++++++ .../src/routes/Community/containers/Table.js | 43 -------- .../src/routes/Stories/components/Stories.js | 6 +- 11 files changed, 202 insertions(+), 236 deletions(-) create mode 100644 client/coral-admin/src/routes/Community/components/Community.css create mode 100644 client/coral-admin/src/routes/Community/containers/People.js delete mode 100644 client/coral-admin/src/routes/Community/containers/Table.js diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 921f0a0a2..b7102b8ff 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -1,9 +1,9 @@ import queryString from 'query-string'; import { - FETCH_COMMENTERS_REQUEST, - FETCH_COMMENTERS_SUCCESS, - FETCH_COMMENTERS_FAILURE, + FETCH_USERS_REQUEST, + FETCH_USERS_SUCCESS, + FETCH_USERS_FAILURE, SORT_UPDATE, COMMENTERS_NEW_PAGE, SET_ROLE, @@ -16,13 +16,13 @@ import { import t from 'coral-framework/services/i18n'; -export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => { - dispatch(requestFetchAccounts()); +export const fetchUsers = (query = {}) => (dispatch, _, {rest}) => { + dispatch(requestFetchUsers()); rest(`/users?${queryString.stringify(query)}`) .then(({result, page, count, limit, totalPages}) =>{ dispatch({ - type: FETCH_COMMENTERS_SUCCESS, - accounts: result, + type: FETCH_USERS_SUCCESS, + users: result, page, count, limit, @@ -32,12 +32,12 @@ export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => { .catch((error) => { console.error(error); const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); - dispatch({type: FETCH_COMMENTERS_FAILURE, error: errorMessage}); + dispatch({type: FETCH_USERS_FAILURE, error: errorMessage}); }); }; -const requestFetchAccounts = () => ({ - type: FETCH_COMMENTERS_REQUEST +const requestFetchUsers = () => ({ + type: FETCH_USERS_REQUEST }); export const updateSorting = (sort) => ({ diff --git a/client/coral-admin/src/constants/community.js b/client/coral-admin/src/constants/community.js index be11e9b0c..dc11572ed 100644 --- a/client/coral-admin/src/constants/community.js +++ b/client/coral-admin/src/constants/community.js @@ -1,6 +1,7 @@ -export const FETCH_COMMENTERS_REQUEST = 'FETCH_COMMENTERS_REQUEST'; -export const FETCH_COMMENTERS_SUCCESS = 'FETCH_COMMENTERS_SUCCESS'; -export const FETCH_COMMENTERS_FAILURE = 'FETCH_COMMENTERS_FAILURE'; +export const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST'; +export const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS'; +export const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE'; + export const SORT_UPDATE = 'SORT_UPDATE'; export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE'; export const SET_ROLE = 'SET_ROLE'; diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js index 9f724c8d6..31fb6aa17 100644 --- a/client/coral-admin/src/reducers/community.js +++ b/client/coral-admin/src/reducers/community.js @@ -1,7 +1,7 @@ import { - FETCH_COMMENTERS_REQUEST, - FETCH_COMMENTERS_FAILURE, - FETCH_COMMENTERS_SUCCESS, + FETCH_USERS_REQUEST, + FETCH_USERS_SUCCESS, + FETCH_USERS_FAILURE, SORT_UPDATE, SET_ROLE, SET_COMMENTER_STATUS, @@ -15,7 +15,7 @@ const initialState = { community: {}, isFetchingPeople: false, errorPeople: '', - accounts: [], + users: [], fieldPeople: 'created_at', ascPeople: false, totalPagesPeople: 0, @@ -27,19 +27,19 @@ const initialState = { export default function community (state = initialState, action) { switch (action.type) { - case FETCH_COMMENTERS_REQUEST : + case FETCH_USERS_REQUEST : return { ...state, isFetchingPeople: true, }; - case FETCH_COMMENTERS_FAILURE : + case FETCH_USERS_FAILURE : return { ...state, isFetchingPeople: false, errorPeople: action.error, }; - case FETCH_COMMENTERS_SUCCESS : { - const {accounts, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line + case FETCH_USERS_SUCCESS : { + const {users, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line return { ...state, isFetchingPeople: false, @@ -49,27 +49,27 @@ export default function community (state = initialState, action) { limitPeople: limit, totalPagesPeople: totalPages, ...rest, - accounts, // Sets to normal array + users, // Sets to normal array }; } case SET_ROLE : { - const commenters = state.accounts; + const commenters = state.users; const idx = commenters.findIndex((el) => el.id === action.id); commenters[idx].roles[0] = action.role; return { ...state, - accounts: commenters.map((id) => id), + users: commenters.map((id) => id), }; } case SET_COMMENTER_STATUS: { - const commenters = state.accounts; + const commenters = state.users; const idx = commenters.findIndex((el) => el.id === action.id); commenters[idx].status = action.status; return { ...state, - accounts: commenters.map((id) => id), + users: commenters.map((id) => id), }; } diff --git a/client/coral-admin/src/routes/Community/components/Community.css b/client/coral-admin/src/routes/Community/components/Community.css new file mode 100644 index 000000000..8f7641418 --- /dev/null +++ b/client/coral-admin/src/routes/Community/components/Community.css @@ -0,0 +1,4 @@ +.container { + max-width: 1280px; + margin: 0 auto; +} \ No newline at end of file diff --git a/client/coral-admin/src/routes/Community/components/Community.js b/client/coral-admin/src/routes/Community/components/Community.js index bf300a166..1b0c6cc2a 100644 --- a/client/coral-admin/src/routes/Community/components/Community.js +++ b/client/coral-admin/src/routes/Community/components/Community.js @@ -1,78 +1,18 @@ import React, {Component} from 'react'; - -import CommunityMenu from './CommunityMenu'; -import People from './People'; -import FlaggedAccounts from '../containers/FlaggedAccounts'; -import RejectUsernameDialog from './RejectUsernameDialog'; import PropTypes from 'prop-types'; +import styles from './Community.css'; +import People from '../containers/People'; +import CommunityMenu from './CommunityMenu'; +import RejectUsernameDialog from './RejectUsernameDialog'; +import FlaggedAccounts from '../containers/FlaggedAccounts'; -export default class Community extends Component { - - state = { - searchValue: '', - timer: null - }; - - onKeyDownHandler = (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - this.search(); - } - } - - onSearchChange = (e) => { - const value = e.target.value; - this.setState((prevState) => { - prevState.searchValue = value; - clearTimeout(prevState.timer); - const fetchAccounts = this.props.fetchAccounts; - prevState.timer = setTimeout(() => { - fetchAccounts({value}); - }, 350); - return prevState; - }); - } - - onHeaderClickHandler = (sort) => { - this.props.updateSorting(sort); - this.search(); - } - - onNewPageHandler = (page) => { - this.props.newPage(page); - this.search({page}); - } - - search(query = {}) { - const {community} = this.props; - - this.props.fetchAccounts({ - value: this.state.searchValue, - field: community.fieldPeople, - asc: community.ascPeople, - ...query - }); - } - - getTabContent(searchValue, props) { - const {community} = props; - const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path; +class Community extends Component { + renderTab() { + const {route, community, ...props} = this.props; + const activeTab = route.path === ':id' ? 'flagged' : route.path; if (activeTab === 'people') { - return ( - - ); + return ; } return ( @@ -82,37 +22,34 @@ export default class Community extends Component { root={this.props.root} />
    ); } render() { - const {searchValue} = this.state; - const tab = this.getTabContent(searchValue, this.props); const {root: {flaggedUsernamesCount}} = this.props; return (
    -
    {tab}
    +
    + {this.renderTab()} +
    ); } } Community.propTypes = { - community: PropTypes.object, - fetchAccounts: PropTypes.func, - hideRejectUsernameDialog: PropTypes.func, - updateSorting: PropTypes.func, - newPage: PropTypes.func, route: PropTypes.object, - rejectUsername: PropTypes.func, + community: PropTypes.object, data: PropTypes.object, - root: PropTypes.object + root: PropTypes.object, }; + +export default Community; diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js index b0524f273..3edf620db 100644 --- a/client/coral-admin/src/routes/Community/components/People.js +++ b/client/coral-admin/src/routes/Community/components/People.js @@ -1,33 +1,24 @@ import React from 'react'; - import styles from './styles.css'; -import Table from '../containers/Table'; -import {Pager, Icon} from 'coral-ui'; +import Table from './Table'; +import {Paginate, Icon} from 'coral-ui'; import EmptyCard from '../../../components/EmptyCard'; import t from 'coral-framework/services/i18n'; + import PropTypes from 'prop-types'; -const tableHeaders = [ - { - title: t('community.username_and_email'), - field: 'username' - }, - { - title: t('community.account_creation_date'), - field: 'created_at' - }, - { - title: t('community.status'), - field: 'status' - }, - { - title: t('community.newsroom_role'), - field: 'role' - } -]; +const People = (props) => { + const { + users = [], + searchValue, + onSearchChange, + onHeaderClickHandler, + onNewPageHandler, + totalPages, + } = props; + + const hasResults = !!users.length; -const People = ({commenters, searchValue, onSearchChange, ...props}) => { - const hasResults = !!commenters.length; return (
    @@ -47,16 +38,15 @@ const People = ({commenters, searchValue, onSearchChange, ...props}) => { { hasResults ? : {t('community.no_results')} } - @@ -64,7 +54,8 @@ const People = ({commenters, searchValue, onSearchChange, ...props}) => { }; People.propTypes = { - commenters: PropTypes.array, + onHeaderClickHandler: PropTypes.func, + users: PropTypes.array, searchValue: PropTypes.string, onSearchChange: PropTypes.func, totalPages: PropTypes.number, diff --git a/client/coral-admin/src/routes/Community/components/Table.js b/client/coral-admin/src/routes/Community/components/Table.js index 28b20b82f..9071e0575 100644 --- a/client/coral-admin/src/routes/Community/components/Table.js +++ b/client/coral-admin/src/routes/Community/components/Table.js @@ -5,7 +5,26 @@ import PropTypes from 'prop-types'; import {Dropdown, Option} from 'coral-ui'; import cn from 'classnames'; -const Table = ({headers, commenters, onHeaderClickHandler, onRoleChange, onCommenterStatusChange, viewUserDetail}) => ( +const headers = [ + { + title: t('community.username_and_email'), + field: 'username' + }, + { + title: t('community.account_creation_date'), + field: 'created_at' + }, + { + title: t('community.status'), + field: 'status' + }, + { + title: t('community.newsroom_role'), + field: 'role' + } +]; + +const Table = ({users, onHeaderClickHandler, onRoleChange, onCommenterStatusChange, viewUserDetail}) => (
    @@ -21,7 +40,7 @@ const Table = ({headers, commenters, onHeaderClickHandler, onRoleChange, onComme - {commenters.map((row, i)=> ( + {users.map((row, i)=> (
    @@ -57,9 +76,8 @@ const Table = ({headers, commenters, onHeaderClickHandler, onRoleChange, onComme ); Table.propTypes = { - headers: PropTypes.array, - commenters: PropTypes.array, - onHeaderClickHandler: PropTypes.func, + users: PropTypes.array, + onHeaderClickHandler: PropTypes.func.isRequired, onRoleChange: PropTypes.func, onCommenterStatusChange: PropTypes.func, viewUserDetail: PropTypes.func, diff --git a/client/coral-admin/src/routes/Community/containers/Community.js b/client/coral-admin/src/routes/Community/containers/Community.js index 6e20a3167..4cf9eeb8a 100644 --- a/client/coral-admin/src/routes/Community/containers/Community.js +++ b/client/coral-admin/src/routes/Community/containers/Community.js @@ -1,65 +1,21 @@ -import React, {Component} from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import PropTypes from 'prop-types'; import {compose, gql} from 'react-apollo'; import withQuery from 'coral-framework/hocs/withQuery'; import {getDefinitionName} from 'coral-framework/utils'; import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations'; import FlaggedAccounts from '../containers/FlaggedAccounts'; import FlaggedUser from '../containers/FlaggedUser'; - -import { - fetchAccounts, - updateSorting, - newPage, - hideRejectUsernameDialog -} from '../../../actions/community'; - +import {hideRejectUsernameDialog} from '../../../actions/community'; import Community from '../components/Community'; -class CommunityContainer extends Component { - - componentWillMount() { - this.props.fetchAccounts({}); - } - - render() { - return ; - } -} const mapStateToProps = (state) => ({ community: state.community, }); -CommunityContainer.propTypes = { - community: PropTypes.object, - fetchAccounts: PropTypes.func, - hideRejectUsernameDialog: PropTypes.func, - updateSorting: PropTypes.func, - newPage: PropTypes.func, - route: PropTypes.object, - rejectUsername: PropTypes.func, - data: PropTypes.object, - root: PropTypes.object -}; - const mapDispatchToProps = (dispatch) => bindActionCreators({ - fetchAccounts, hideRejectUsernameDialog, - updateSorting, - newPage, }, dispatch); const withData = withQuery(gql` @@ -89,4 +45,4 @@ export default compose( withSetUserStatus, withRejectUsername, withData -)(CommunityContainer); +)(Community); diff --git a/client/coral-admin/src/routes/Community/containers/People.js b/client/coral-admin/src/routes/Community/containers/People.js new file mode 100644 index 000000000..fb383917f --- /dev/null +++ b/client/coral-admin/src/routes/Community/containers/People.js @@ -0,0 +1,102 @@ +import React from 'react'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import People from '../components/People'; +import PropTypes from 'prop-types'; + +import {viewUserDetail} from '../../../actions/userDetail'; + +import { + fetchUsers, + updateSorting, + newPage, + hideRejectUsernameDialog, + setCommenterStatus, + setRole, +} from '../../../actions/community'; + +class PeopleContainer extends React.Component { + state = { + searchValue: '', + timer: null + }; + + componentWillMount() { + this.props.fetchUsers(); + } + + onKeyDownHandler = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + this.search(); + } + } + + onSearchChange = (e) => { + const value = e.target.value; + + this.setState((prevState) => { + prevState.searchValue = value; + clearTimeout(prevState.timer); + const fetchAccounts = this.props.fetchUsers; + prevState.timer = setTimeout(() => { + fetchAccounts({value}); + }, 350); + return prevState; + }); + } + + onHeaderClickHandler = (sort) => { + this.props.updateSorting(sort); + this.search(); + } + + onNewPageHandler = ({selected}) => { + const page = selected + 1; + this.props.newPage(page); + this.search({page}); + } + + search(query = {}) { + const {community} = this.props; + + this.props.fetchUsers({ + value: this.state.searchValue, + field: community.fieldPeople, + asc: community.ascPeople, + ...query + }); + } + render() { + return ; + } +} + +PeopleContainer.propTypes = { + newPage: PropTypes.func, + fetchUsers: PropTypes.func, + updateSorting: PropTypes.func, + setRole: PropTypes.func, + setCommenterStatus: PropTypes.func, + community: PropTypes.object, +}; + +const mapDispatchToProps = (dispatch) => + bindActionCreators({ + newPage, + fetchUsers, + updateSorting, + hideRejectUsernameDialog, + setCommenterStatus, + setRole, + viewUserDetail, + }, dispatch); + +export default connect(null, mapDispatchToProps)(PeopleContainer); diff --git a/client/coral-admin/src/routes/Community/containers/Table.js b/client/coral-admin/src/routes/Community/containers/Table.js deleted file mode 100644 index 086a56638..000000000 --- a/client/coral-admin/src/routes/Community/containers/Table.js +++ /dev/null @@ -1,43 +0,0 @@ -import React, {Component} from 'react'; -import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; -import {setRole, setCommenterStatus} from '../../../actions/community'; -import Table from '../components/Table'; -import {viewUserDetail} from '../../../actions/userDetail'; -import PropTypes from 'prop-types'; - -class TableContainer extends Component { - - constructor (props) { - super(props); - } - - render () { - return ; - } -} - -TableContainer.propTypes = { - setRole: PropTypes.func, - setCommenterStatus: PropTypes.func, - commenters: PropTypes.array, -}; - -const mapStateToProps = (state) => ({ - commenters: state.community.accounts, -}); - -const mapDispatchToProps = (dispatch) => - bindActionCreators({ - setCommenterStatus, - setRole, - viewUserDetail, - }, dispatch); - -export default connect(mapStateToProps, mapDispatchToProps)(TableContainer); - diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index 746c733d7..57caa2744 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -2,7 +2,7 @@ import React, {Component} from 'react'; import {Link} from 'react-router'; import PropTypes from 'prop-types'; import sortBy from 'lodash/sortBy'; -import {Dropdown, Option, Pager, Icon} from 'coral-ui'; +import {Dropdown, Option, Paginate, Icon} from 'coral-ui'; import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl'; import t from 'coral-framework/services/i18n'; import styles from './Stories.css'; @@ -139,10 +139,10 @@ class Stories extends Component { {t('streams.status')} - + onNewPageHandler={this.onPageClick} /> */} : {t('streams.empty_result')} } From 9f4a91365511141b2b39411e74999dcae108ce0b Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 24 Oct 2017 00:03:35 -0300 Subject: [PATCH 06/40] Passing props --- .../src/routes/Community/components/Community.js | 2 ++ .../src/routes/Community/components/People.js | 6 ++++++ .../src/routes/Community/components/Table.js | 10 +++++----- .../src/routes/Community/containers/People.js | 6 ++++-- package.json | 1 + routes/api/users/index.js | 2 +- script | 0 script.sh | 4 ++++ 8 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 script create mode 100755 script.sh diff --git a/client/coral-admin/src/routes/Community/components/Community.js b/client/coral-admin/src/routes/Community/components/Community.js index 1b0c6cc2a..bd41fbbea 100644 --- a/client/coral-admin/src/routes/Community/components/Community.js +++ b/client/coral-admin/src/routes/Community/components/Community.js @@ -48,6 +48,8 @@ class Community extends Component { Community.propTypes = { route: PropTypes.object, community: PropTypes.object, + rejectUsername: PropTypes.func.isRequired, + hideRejectUsernameDialog: PropTypes.func.isRequired, data: PropTypes.object, root: PropTypes.object, }; diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js index 3edf620db..2be222c05 100644 --- a/client/coral-admin/src/routes/Community/components/People.js +++ b/client/coral-admin/src/routes/Community/components/People.js @@ -15,6 +15,8 @@ const People = (props) => { onHeaderClickHandler, onNewPageHandler, totalPages, + setRole, + setCommenterStatus, } = props; const hasResults = !!users.length; @@ -39,6 +41,8 @@ const People = (props) => { hasResults ?
    : {t('community.no_results')} @@ -60,6 +64,8 @@ People.propTypes = { onSearchChange: PropTypes.func, totalPages: PropTypes.number, onNewPageHandler: PropTypes.func, + setCommenterStatus: PropTypes.func.isRequired, + setRole: PropTypes.func.isRequired, }; export default People; diff --git a/client/coral-admin/src/routes/Community/components/Table.js b/client/coral-admin/src/routes/Community/components/Table.js index 9071e0575..28a3e4c42 100644 --- a/client/coral-admin/src/routes/Community/components/Table.js +++ b/client/coral-admin/src/routes/Community/components/Table.js @@ -24,7 +24,7 @@ const headers = [ } ]; -const Table = ({users, onHeaderClickHandler, onRoleChange, onCommenterStatusChange, viewUserDetail}) => ( +const Table = ({users, onHeaderClickHandler, setRole, setCommenterStatus, viewUserDetail}) => (
    @@ -53,7 +53,7 @@ const Table = ({users, onHeaderClickHandler, onRoleChange, onCommenterStatusChan onCommenterStatusChange(row.id, status)}> + onChange={(status) => setCommenterStatus(row.id, status)}> @@ -62,7 +62,7 @@ const Table = ({users, onHeaderClickHandler, onRoleChange, onCommenterStatusChan onRoleChange(row.id, role)}> + onChange={(role) => setRole(row.id, role)}>
    : {t('community.no_results')} } - + ( +const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUserDetail}) => (
    {headers.map((header, i) =>( + + {users.map((row, i)=> ( + + + + + + + ))} + +
    onHeaderClickHandler({field: header.field})}> {header.title} @@ -80,7 +80,7 @@ Table.propTypes = { onHeaderClickHandler: PropTypes.func.isRequired, setRole: PropTypes.func.isRequired, setCommenterStatus: PropTypes.func.isRequired, - viewUserDetail: PropTypes.func, + viewUserDetail: PropTypes.func.isRequired, }; export default Table; diff --git a/client/coral-admin/src/routes/Community/containers/People.js b/client/coral-admin/src/routes/Community/containers/People.js index c45361c9f..5cd6f6e9c 100644 --- a/client/coral-admin/src/routes/Community/containers/People.js +++ b/client/coral-admin/src/routes/Community/containers/People.js @@ -77,6 +77,7 @@ class PeopleContainer extends React.Component { totalPages={this.props.community.totalPagesPeople} setCommenterStatus={this.props.setCommenterStatus} setRole={this.props.setRole} + viewUserDetail={this.props.viewUserDetail} />; } } @@ -87,6 +88,7 @@ PeopleContainer.propTypes = { updateSorting: PropTypes.func, setRole: PropTypes.func.isRequired, setCommenterStatus: PropTypes.func.isRequired, + viewUserDetail: PropTypes.func.isRequired, community: PropTypes.object, }; diff --git a/client/coral-ui/components/Paginate.css b/client/coral-ui/components/Paginate.css index dbb25b844..86b7c91c5 100644 --- a/client/coral-ui/components/Paginate.css +++ b/client/coral-ui/components/Paginate.css @@ -2,7 +2,7 @@ text-align: center; } -.page, .previous, .next { +.page, .previous, .next, .break { display: inline-block; margin-right: 5px; height: 30px; diff --git a/client/coral-ui/components/Paginate.js b/client/coral-ui/components/Paginate.js index 1d7d55398..3a404db69 100644 --- a/client/coral-ui/components/Paginate.js +++ b/client/coral-ui/components/Paginate.js @@ -11,7 +11,6 @@ const Paginate = ({pageCount, onPageChange}) => ( pageRangeDisplayed={5} marginPagesDisplayed={2} onPageChange={onPageChange} - breakLabel={...} breakClassName={styles.break} containerClassName={styles.container} pageClassName={styles.page} From 0b973f24ff456d98ddaa1289716f019ed703e171 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 24 Oct 2017 00:14:47 -0300 Subject: [PATCH 08/40] Replacing paginate from Stories --- .../coral-admin/src/routes/Stories/components/Stories.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index 57caa2744..966e8a9c3 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -139,10 +139,9 @@ class Stories extends Component { {t('streams.status')} - {/* */} + : {t('streams.empty_result')} } From 34334b81521bd6686d8134e5c5e222f305581303 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 24 Oct 2017 00:18:03 -0300 Subject: [PATCH 09/40] Pagination only shows when there are results to show --- .../src/routes/Community/components/People.js | 9 +- .../src/routes/Community/components/Table.js | 106 ++++++++++-------- 2 files changed, 60 insertions(+), 55 deletions(-) diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js index 0878873ac..9626dd53f 100644 --- a/client/coral-admin/src/routes/Community/components/People.js +++ b/client/coral-admin/src/routes/Community/components/People.js @@ -1,7 +1,7 @@ import React from 'react'; import styles from './styles.css'; import Table from './Table'; -import {Paginate, Icon} from 'coral-ui'; +import {Icon} from 'coral-ui'; import EmptyCard from '../../../components/EmptyCard'; import t from 'coral-framework/services/i18n'; @@ -46,14 +46,11 @@ const People = (props) => { viewUserDetail={viewUserDetail} setCommenterStatus={setCommenterStatus} onHeaderClickHandler={onHeaderClickHandler} + pageCount={totalPages} + onPageChange={onNewPageHandler} /> : {t('community.no_results')} } - - ); diff --git a/client/coral-admin/src/routes/Community/components/Table.js b/client/coral-admin/src/routes/Community/components/Table.js index 33cbb87e8..8d650f165 100644 --- a/client/coral-admin/src/routes/Community/components/Table.js +++ b/client/coral-admin/src/routes/Community/components/Table.js @@ -2,7 +2,7 @@ import React from 'react'; import styles from './Table.css'; import t from 'coral-framework/services/i18n'; import PropTypes from 'prop-types'; -import {Dropdown, Option} from 'coral-ui'; +import {Paginate, Dropdown, Option} from 'coral-ui'; import cn from 'classnames'; const headers = [ @@ -24,55 +24,61 @@ const headers = [ } ]; -const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUserDetail}) => ( - - - - {headers.map((header, i) =>( - - ))} - - - - {users.map((row, i)=> ( - - - - - +const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUserDetail, pageCount, onPageChange}) => ( +
    +
    onHeaderClickHandler({field: header.field})}> - {header.title} -
    - - {row.profiles.map(({id}) => id)} - - {row.created_at} - - setCommenterStatus(row.id, status)}> - - - setRole(row.id, role)}> - -
    + + + {headers.map((header, i) =>( + + ))} - ))} - -
    onHeaderClickHandler({field: header.field})}> + {header.title} +
    +
    + + {row.profiles.map(({id}) => id)} + + {row.created_at} + + setCommenterStatus(row.id, status)}> + + + setRole(row.id, role)}> + +
    + + ); Table.propTypes = { @@ -81,6 +87,8 @@ Table.propTypes = { setRole: PropTypes.func.isRequired, setCommenterStatus: PropTypes.func.isRequired, viewUserDetail: PropTypes.func.isRequired, + pageCount: PropTypes.number.isRequired, + onPageChange: PropTypes.func.isRequired, }; export default Table; From 38abf63cc9d9ee9bb7228c58595a1ae689c1c988 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 24 Oct 2017 00:21:26 -0300 Subject: [PATCH 10/40] removing unused files --- script | 0 script.sh | 4 ---- 2 files changed, 4 deletions(-) delete mode 100644 script delete mode 100755 script.sh diff --git a/script b/script deleted file mode 100644 index e69de29bb..000000000 diff --git a/script.sh b/script.sh deleted file mode 100755 index 156d7fb27..000000000 --- a/script.sh +++ /dev/null @@ -1,4 +0,0 @@ -for i in {1..100} -do - ./bin/cli -c .env users create --email "user-x${i}@coralproject.net" --password "mysecretpassword" --name "userxxx${i}" -f -done From b189c9d2b0f8011faeff5746371e467b56449b60 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 24 Oct 2017 12:08:46 +0200 Subject: [PATCH 11/40] Add cursor pointer and increase clickable area --- client/coral-ui/components/Paginate.css | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/client/coral-ui/components/Paginate.css b/client/coral-ui/components/Paginate.css index 86b7c91c5..813786d66 100644 --- a/client/coral-ui/components/Paginate.css +++ b/client/coral-ui/components/Paginate.css @@ -4,18 +4,20 @@ .page, .previous, .next, .break { display: inline-block; + list-style: none; margin-right: 5px; +} + +.pageLink, .previousLink, .nextLink { + display: inline-block; + color: #696969; + cursor: pointer; height: 30px; text-align: center; vertical-align: middle; line-height: 30px; width: 30px; - list-style: none; -} - -.pageLink, .previousLink, .nextLink { - color: #696969; - cursor: default; + user-select: none; } .previousLink, .nextLink { From 46359b4a1b6d49fe19e85a98784510f0f949d4de Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 24 Oct 2017 14:49:32 +0200 Subject: [PATCH 12/40] Show errors --- client/coral-admin/src/components/UserDetail.js | 4 ---- client/coral-admin/src/containers/UserDetail.js | 14 ++++---------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index 1c6362791..140774eae 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -44,7 +44,6 @@ export default class UserDetail extends React.Component { this.props.data.refetch(); } catch (err) { - // TODO: handle error. console.error(err); this.props.notify('error', getErrorMessages(err)); } @@ -56,7 +55,6 @@ export default class UserDetail extends React.Component { this.props.data.refetch(); } catch (err) { - // TODO: handle error. console.error(err); this.props.notify('error', getErrorMessages(err)); } @@ -68,7 +66,6 @@ export default class UserDetail extends React.Component { this.props.data.refetch(); } catch (err) { - // TODO: handle error. console.error(err); this.props.notify('error', getErrorMessages(err)); } @@ -80,7 +77,6 @@ export default class UserDetail extends React.Component { this.props.data.refetch(); } catch (err) { - // TODO: handle error. console.error(err); this.props.notify('error', getErrorMessages(err)); } diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index 47d8e1c79..97021ec75 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -43,22 +43,16 @@ class UserDetailContainer extends React.Component { return this.props.setCommentStatus({commentId, status}); }); - try { - await Promise.all(changes); - this.props.clearUserDetailSelections(); // un-select everything - } catch (err) { - - // TODO: handle error. - console.error(err); - } + await Promise.all(changes); + this.props.clearUserDetailSelections(); // un-select everything } bulkReject = () => { - this.bulkSetCommentStatus('REJECTED'); + return this.bulkSetCommentStatus('REJECTED'); } bulkAccept = () => { - this.bulkSetCommentStatus('ACCEPTED'); + return this.bulkSetCommentStatus('ACCEPTED'); } acceptComment = ({commentId}) => { From 24ec898a15496d501111b5345e716e16c785461f Mon Sep 17 00:00:00 2001 From: Kim Gardner Date: Tue, 24 Oct 2017 14:14:26 +0100 Subject: [PATCH 13/40] Fix formatting --- docs/_docs/03-02-product-guide-commenter-features.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_docs/03-02-product-guide-commenter-features.md b/docs/_docs/03-02-product-guide-commenter-features.md index b46c5f054..7f56880db 100644 --- a/docs/_docs/03-02-product-guide-commenter-features.md +++ b/docs/_docs/03-02-product-guide-commenter-features.md @@ -7,9 +7,9 @@ permalink: /commenter-features/ There are 2 ways that newsrooms can support signup/login functionality with Talk: -*Use Talk’s auth plugin out of the box (supports account registration with username and password, as well as features like forgot password) +* Use Talk’s auth plugin out of the box (supports account registration with username and password, as well as features like forgot password) -*Create their own auth plugin to integrate with your own auth systems +* Create their own auth plugin to integrate with your own auth systems We also provide a Facebook auth plugin that supports logging in with Facebook (you must provide your own Facebook App ID and Secret, which you can read more about here: [https://developers.facebook.com](https://developers.facebook.com){:target="_blank"}) From b7b9da9509e290694bf2d9e689cb9bdc28022767 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 24 Oct 2017 15:16:45 +0200 Subject: [PATCH 14/40] Change fetchPolicy --- client/coral-admin/src/containers/UserDetail.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/coral-admin/src/containers/UserDetail.js b/client/coral-admin/src/containers/UserDetail.js index 97021ec75..3a458ff83 100644 --- a/client/coral-admin/src/containers/UserDetail.js +++ b/client/coral-admin/src/containers/UserDetail.js @@ -165,7 +165,8 @@ export const withUserDetailQuery = withQuery(gql` `, { options: ({userId, statuses}) => { return { - variables: {author_id: userId, statuses} + variables: {author_id: userId, statuses}, + fetchPolicy: 'network-only', }; }, skip: (ownProps) => !ownProps.userId, From fed6654f5208bcc62a6bcb32bc8d35238d67f4d9 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 24 Oct 2017 10:45:25 -0300 Subject: [PATCH 15/40] Support pagination for search --- routes/api/users/index.js | 18 ++++++++++++------ services/users.js | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/routes/api/users/index.js b/routes/api/users/index.js index 00b776e5b..0cf1c997b 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -9,8 +9,8 @@ const { ROOT_URL } = require('../../../config'); -// get a list of users. router.get('/', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => { + const { value = '', field = 'created_at', @@ -21,13 +21,20 @@ router.get('/', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, nex try { + const queryOpts = { + sort: {[field]: (asc === 'true') ? 1 : -1}, + skip: (page - 1) * limit, + limit + }; + let [result, count] = await Promise.all([ UsersService .search(value) - .sort({[field]: (asc === 'true') ? 1 : -1}) - .skip((page - 1) * limit) - .limit(limit), - UsersService.count() + .sort(queryOpts.sort) + .skip(queryOpts.skip) + .limit(queryOpts.limit) + .lean(), + UsersService.search(value).count() ]); res.json({ @@ -41,7 +48,6 @@ router.get('/', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, nex } catch (e) { next(e); } - }); router.post('/:user_id/role', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => { diff --git a/services/users.js b/services/users.js index 7968f7e83..4d974f4a4 100644 --- a/services/users.js +++ b/services/users.js @@ -668,8 +668,8 @@ module.exports = class UsersService { * Returns a count of the current users. * @return {Promise} */ - static count() { - return UserModel.count(); + static count(query = {}) { + return UserModel.count(query); } /** From 55b2208a498235c45e7dcdbe48545ce77535c064 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 24 Oct 2017 11:29:17 -0300 Subject: [PATCH 16/40] Pagination and refactor for Stories --- client/coral-admin/src/actions/assets.js | 13 ++-- client/coral-admin/src/reducers/assets.js | 2 + .../src/routes/Stories/components/Stories.js | 72 +++++++++++++------ .../src/routes/Stories/containers/Stories.js | 16 +---- routes/api/assets/index.js | 30 +++++--- routes/api/users/index.js | 4 +- 6 files changed, 86 insertions(+), 51 deletions(-) diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/assets.js index 1ce6ad6ac..d7c6d9448 100644 --- a/client/coral-admin/src/actions/assets.js +++ b/client/coral-admin/src/actions/assets.js @@ -1,3 +1,5 @@ +import queryString from 'query-string'; + import { FETCH_ASSETS_REQUEST, FETCH_ASSETS_SUCCESS, @@ -16,13 +18,16 @@ import t from 'coral-framework/services/i18n'; // Fetch a page of assets // Get comments to fill each of the three lists on the mod queue -export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch, _, {rest}) => { +export const fetchAssets = (query = {}) => (dispatch, _, {rest}) => { dispatch({type: FETCH_ASSETS_REQUEST}); - return rest(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`) - .then(({result, count}) => + return rest(`/assets?${queryString.stringify(query)}`) + .then(({result, page, count, limit, totalPages}) => dispatch({type: FETCH_ASSETS_SUCCESS, assets: result, - count + page, + count, + limit, + totalPages, })) .catch((error) => { console.error(error); diff --git a/client/coral-admin/src/reducers/assets.js b/client/coral-admin/src/reducers/assets.js index bb8751973..b4ea8d21f 100644 --- a/client/coral-admin/src/reducers/assets.js +++ b/client/coral-admin/src/reducers/assets.js @@ -16,6 +16,8 @@ export default function assets (state = initialState, action) { }, {}); return update(state, { + totalPages: {$set: action.totalPages}, + page: {$set: action.page}, byId: {$set: assets}, count: {$set: action.count}, ids: {$set: Object.keys(assets)}, diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index 966e8a9c3..c37a7f8f6 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -8,38 +8,54 @@ import t from 'coral-framework/services/i18n'; import styles from './Stories.css'; import EmptyCard from 'coral-admin/src/components/EmptyCard'; -const limit = 25; - class Stories extends Component { state = { - search: '', + searchValue: '', sort: 'desc', filter: 'all', statusMenus: {}, timer: null, - page: 0 } componentDidMount () { - this.props.fetchAssets(0, limit, '', this.state.sortBy); + const {sort} = this.state; + + this.props.fetchAssets({ + sort, + }); } onSettingChange = (setting) => (e) => { - let options = this.state; - this.setState({[setting]: e.target.value}); - options[setting] = e.target.value; - this.props.fetchAssets(0, limit, options.search, options.sort, options.filter); + const {searchValue, sort, filter} = this.state; + + this.setState({ + [setting]: e.target.value + }); + + this.props.fetchAssets({ + value: searchValue, + sort, + filter, + }); } onSearchChange = (e) => { - const search = e.target.value; + const {fetchAssets} = this.props; + const {value} = e.target; + const {sort, filter, limit} = this.state; + this.setState((prevState) => { - prevState.search = search; + prevState.searchValue = value; clearTimeout(prevState.timer); - const fetchAssets = this.props.fetchAssets; + prevState.timer = setTimeout(() => { - fetchAssets(0, limit, search, this.state.sort, this.state.filter); + fetchAssets({ + value, + sort, + filter, + limit, + }); }, 350); return prevState; }); @@ -51,10 +67,17 @@ class Stories extends Component { } onStatusChange = async (closeStream, id) => { + const {fetchAssets, updateAssetState} = this.props; + const {searchValue, sort, filter, limit} = this.state; + try { - this.props.updateAssetState(id, closeStream ? Date.now() : null); - const {search, sort, filter, page} = this.state; - this.props.fetchAssets(page, limit, search, sort, filter); + updateAssetState(id, closeStream ? Date.now() : null); + fetchAssets({ + value: searchValue, + sort, + filter, + limit, + }); } catch(err) { console.error(err); } @@ -74,10 +97,17 @@ class Stories extends Component { ); } - onPageClick = (page) => { - this.setState({page}); - const {search, sort, filter} = this.state; - this.props.fetchAssets((page - 1) * limit, limit, search, sort, filter); + onPageClick = ({selected}) => { + const page = selected + 1; + const {searchValue, sort, filter, limit} = this.state; + + this.props.fetchAssets({ + page, + value: searchValue, + sort, + filter, + limit, + }); } render () { @@ -140,7 +170,7 @@ class Stories extends Component { : {t('streams.empty_result')} diff --git a/client/coral-admin/src/routes/Stories/containers/Stories.js b/client/coral-admin/src/routes/Stories/containers/Stories.js index f645f310c..b940e46e6 100644 --- a/client/coral-admin/src/routes/Stories/containers/Stories.js +++ b/client/coral-admin/src/routes/Stories/containers/Stories.js @@ -1,19 +1,9 @@ -import React, {Component} from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {compose} from 'react-apollo'; import {fetchAssets, updateAssetState} from 'coral-admin/src/actions/assets'; import Stories from '../components/Stories'; -class StoriesContainer extends Component { - render () { - return ; - } -} - -const mapStateToProps = (state) => ({ - assets: state.assets -}); +const mapStateToProps = ({assets}) => ({assets}); const mapDispatchToProps = (dispatch) => bindActionCreators({ @@ -21,7 +11,5 @@ const mapDispatchToProps = (dispatch) => updateAssetState, }, dispatch); -export default compose( - connect(mapStateToProps, mapDispatchToProps), -)(StoriesContainer); +export default connect(mapStateToProps, mapDispatchToProps)(Stories); diff --git a/routes/api/assets/index.js b/routes/api/assets/index.js index 1b2b0c7a9..4007517de 100644 --- a/routes/api/assets/index.js +++ b/routes/api/assets/index.js @@ -37,34 +37,44 @@ const FilterOpenAssets = (query, filter) => { router.get('/', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => { const { - limit = 20, - skip = 0, - sort = 'asc', + value = '', field = 'created_at', + page = 1, + asc = 'false', filter = 'all', - search = '' + limit = 20, } = req.query; try { + const queryOpts = { + sort: {[field]: (asc === 'true') ? 1 : -1}, + skip: (page - 1) * limit, + limit + }; + // Find all the assets. let [result, count] = await Promise.all([ // Find the actuall assets. - FilterOpenAssets(AssetsService.search({value: search}), filter) - .sort({[field]: (sort === 'asc') ? 1 : -1}) - .skip(parseInt(skip)) - .limit(parseInt(limit)), + FilterOpenAssets(AssetsService.search({value}), filter) + .sort(queryOpts.sort) + .skip(parseInt(queryOpts.skip)) + .limit(parseInt(queryOpts.limit)) + .lean(), // Get the count of actual assets. - FilterOpenAssets(AssetsService.search({value: search}), filter) + FilterOpenAssets(AssetsService.search({value}), filter) .count() ]); // Send back the asset data. res.json({ result, - count + limit: Number(limit), + count, + page: Number(page), + totalPages: Math.ceil(count / (limit === 0 ? 1 : limit)) }); } catch (e) { return next(e); diff --git a/routes/api/users/index.js b/routes/api/users/index.js index 0cf1c997b..98cdcdead 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -31,8 +31,8 @@ router.get('/', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, nex UsersService .search(value) .sort(queryOpts.sort) - .skip(queryOpts.skip) - .limit(queryOpts.limit) + .skip(parseInt(queryOpts.skip)) + .limit(parseInt(queryOpts.limit)) .lean(), UsersService.search(value).count() ]); From 3777b4b1ce5581ac97b68d77314569f885778a68 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 24 Oct 2017 11:35:27 -0300 Subject: [PATCH 17/40] Filter Streams fully working --- .../src/routes/Stories/components/Stories.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index c37a7f8f6..ed4a7a595 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -27,16 +27,14 @@ class Stories extends Component { } onSettingChange = (setting) => (e) => { - const {searchValue, sort, filter} = this.state; - - this.setState({ - [setting]: e.target.value - }); + const {searchValue} = this.state; + const criteria = {[setting]: e.target.value}; + + this.setState(criteria); this.props.fetchAssets({ value: searchValue, - sort, - filter, + ...criteria, }); } From 61108337ccfd4ca9113b7592920976f49bf943d1 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 24 Oct 2017 11:42:21 -0300 Subject: [PATCH 18/40] Even tinier --- .../src/routes/Stories/components/Stories.js | 47 +++++++------------ 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index ed4a7a595..bd0dd9f86 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -19,11 +19,7 @@ class Stories extends Component { } componentDidMount () { - const {sort} = this.state; - - this.props.fetchAssets({ - sort, - }); + this.fetchAssets(); } onSettingChange = (setting) => (e) => { @@ -39,21 +35,14 @@ class Stories extends Component { } onSearchChange = (e) => { - const {fetchAssets} = this.props; const {value} = e.target; - const {sort, filter, limit} = this.state; this.setState((prevState) => { prevState.searchValue = value; clearTimeout(prevState.timer); prevState.timer = setTimeout(() => { - fetchAssets({ - value, - sort, - filter, - limit, - }); + this.fetchAssets(); }, 350); return prevState; }); @@ -64,18 +53,24 @@ class Stories extends Component { return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`; } - onStatusChange = async (closeStream, id) => { - const {fetchAssets, updateAssetState} = this.props; + fetchAssets = (query) => { const {searchValue, sort, filter, limit} = this.state; + this.props.fetchAssets({ + value: searchValue, + sort, + filter, + limit, + ...query + }); + }; + + onStatusChange = async (closeStream, id) => { + const {updateAssetState} = this.props; + try { updateAssetState(id, closeStream ? Date.now() : null); - fetchAssets({ - value: searchValue, - sort, - filter, - limit, - }); + this.fetchAssets(); } catch(err) { console.error(err); } @@ -97,15 +92,7 @@ class Stories extends Component { onPageClick = ({selected}) => { const page = selected + 1; - const {searchValue, sort, filter, limit} = this.state; - - this.props.fetchAssets({ - page, - value: searchValue, - sort, - filter, - limit, - }); + this.props.fetchAssets({page}); } render () { From f3bf9d28a7dbe4ab285d8664d6da59adb05968ae Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 24 Oct 2017 11:47:18 -0300 Subject: [PATCH 19/40] Search Value --- client/coral-admin/src/routes/Stories/components/Stories.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index bd0dd9f86..84eaf9fbb 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -96,7 +96,7 @@ class Stories extends Component { } render () { - const {search, sort, filter} = this.state; + const {searchValue, sort, filter} = this.state; const {assets} = this.props; const assetsIds = sortBy(assets.ids.map((id) => assets.byId[id]), 'publication_date'); @@ -112,7 +112,7 @@ class Stories extends Component { From ca1ce9ba763a56adc4e564f852c806aeae30bac4 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Tue, 24 Oct 2017 17:23:10 +0200 Subject: [PATCH 20/40] Dont cache auth --- routes/api/auth/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/routes/api/auth/index.js b/routes/api/auth/index.js index 791573898..1af57fb54 100644 --- a/routes/api/auth/index.js +++ b/routes/api/auth/index.js @@ -12,6 +12,10 @@ router.get('/', (req, res, next) => { return; } + res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate'); + res.header('Expires', '-1'); + res.header('Pragma', 'no-cache'); + // Send back the user object. res.json({user: req.user}); }); From a0929fd773e9deeb501b7b32468507e81dd56e18 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 24 Oct 2017 12:30:07 -0300 Subject: [PATCH 21/40] Updated tests --- test/server/routes/api/assets/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/server/routes/api/assets/index.js b/test/server/routes/api/assets/index.js index a07b154d4..0dacccbad 100644 --- a/test/server/routes/api/assets/index.js +++ b/test/server/routes/api/assets/index.js @@ -56,7 +56,7 @@ describe('/api/v1/assets', () => { it('should return assets that we search for', async () => { for (const role of ['ADMIN', 'MODERATOR']) { const res = await chai.request(app) - .get('/api/v1/assets?search=term2') + .get('/api/v1/assets?value=term2') .set(passport.inject({roles: [role]})); const body = res.body; @@ -78,7 +78,7 @@ describe('/api/v1/assets', () => { it('should not return assets that we do not search for', async () => { for (const role of ['ADMIN', 'MODERATOR']) { const res = await chai.request(app) - .get('/api/v1/assets?search=term3') + .get('/api/v1/assets?value=term3') .set(passport.inject({roles: [role]})); const body = res.body; From be994d033b0f99406c9cc3c52752b5f6750abd2f Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 24 Oct 2017 12:58:15 -0300 Subject: [PATCH 22/40] Adding classes --- client/coral-admin/src/components/ui/Header.js | 13 +++++++------ .../src/routes/Community/components/Community.js | 2 +- .../src/routes/Stories/components/Stories.js | 3 ++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index 62f6a7fd1..c5af63786 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -1,4 +1,5 @@ import React from 'react'; +import cn from 'classnames'; import PropTypes from 'prop-types'; import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl'; import {Link, IndexLink} from 'react-router'; @@ -24,7 +25,7 @@ const CoralHeader = ({ {t('configure.dashboard')} @@ -33,7 +34,7 @@ const CoralHeader = ({ can(auth.user, 'MODERATE_COMMENTS') && ( {t('configure.moderate')} @@ -42,8 +43,8 @@ const CoralHeader = ({ ) } {t('configure.stories')} @@ -51,7 +52,7 @@ const CoralHeader = ({ {t('configure.community')} @@ -62,7 +63,7 @@ const CoralHeader = ({ can(auth.user, 'UPDATE_CONFIG') && ( {t('configure.configure')} diff --git a/client/coral-admin/src/routes/Community/components/Community.js b/client/coral-admin/src/routes/Community/components/Community.js index bd41fbbea..3b60b357a 100644 --- a/client/coral-admin/src/routes/Community/components/Community.js +++ b/client/coral-admin/src/routes/Community/components/Community.js @@ -35,7 +35,7 @@ class Community extends Component { const {root: {flaggedUsernamesCount}} = this.props; return ( -
    +
    {this.renderTab()} diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index 84eaf9fbb..73f5c60dc 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -1,4 +1,5 @@ import React, {Component} from 'react'; +import cn from 'classnames'; import {Link} from 'react-router'; import PropTypes from 'prop-types'; import sortBy from 'lodash/sortBy'; @@ -106,7 +107,7 @@ class Stories extends Component { } return ( -
    +
    From 7ef9e2783b71fef6d76421c08a31bde0b5f4e9c1 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 24 Oct 2017 12:58:27 -0300 Subject: [PATCH 23/40] Initial e2e --- test/e2e/page_objects/admin.js | 5 +++++ test/e2e/specs/02_admin.js | 26 +++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/test/e2e/page_objects/admin.js b/test/e2e/page_objects/admin.js index c4fcb83c1..ffdcf8a29 100644 --- a/test/e2e/page_objects/admin.js +++ b/test/e2e/page_objects/admin.js @@ -14,5 +14,10 @@ module.exports = { 'emailInput': '.talk-admin-login-sign-in #email', 'passwordInput': '.talk-admin-login-sign-in #password', 'signInButton': '.talk-admin-login-sign-in-button', + 'storiesNav': '.talk-admin-nav-stories', + 'storiesSection': '.talk-admin-stories', + 'communityNav': '.talk-admin-nav-community', + 'communitySection': '.talk-admin-community', + 'moderationContainer': '.talk-admin-moderation-container' } }; diff --git a/test/e2e/specs/02_admin.js b/test/e2e/specs/02_admin.js index 236a1c6ca..463249eeb 100644 --- a/test/e2e/specs/02_admin.js +++ b/test/e2e/specs/02_admin.js @@ -1,6 +1,10 @@ module.exports = { '@tags': ['admin', 'login'], + beforeEach: (client) => { + // Testing Desktop + client.resizeWindow(1280, 800); + }, 'Admin logs in': (client) => { const adminPage = client.page.admin(); const {testData: {admin}} = client.globals; @@ -12,8 +16,28 @@ module.exports = { .setValue('@emailInput', admin.email) .setValue('@passwordInput', admin.password) .waitForElementVisible('@signInButton') - .click('@signInButton'); + .click('@signInButton') + .waitForElementVisible('@moderationContainer'); + }, + 'Admin goes to Stories': (client) => { + const adminPage = client.page.admin(); + + adminPage + .navigate() + .waitForElementVisible('@storiesNav') + .click('@storiesNav') + .waitForElementVisible('@storiesSection'); + }, + + 'Admin goes to Community': (client) => { + const adminPage = client.page.admin(); + + adminPage + .navigate() + .waitForElementVisible('@communityNav') + .click('@communityNav') + .waitForElementVisible('@communitySection'); }, after: (client) => { client.end(); From dd7ca9fd0c06cb4d695a136a980bf5f8a28499e0 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 23 Oct 2017 11:24:02 -0600 Subject: [PATCH 24/40] added docs --- services/limit.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/services/limit.js b/services/limit.js index 7acc78f0c..9d1101643 100644 --- a/services/limit.js +++ b/services/limit.js @@ -3,6 +3,9 @@ const errors = require('../errors'); const {createClientFactory} = require('./redis'); const client = createClientFactory(); +/** + * Limit is designed to support rate limiting a resource. + */ class Limit { constructor(prefix, max, duration) { this.ttl = ms(duration) / 1000; @@ -10,16 +13,37 @@ class Limit { this.max = max; } + /** + * key will compose the redis key used to store the rate limit information. + * + * @param {String} value the string to use that is being limited + * @returns {String} the redis key to set + */ key(value) { return `limit[${this.prefix}][${value}]`; } + /** + * get will fetch the current number of attempts within the given window + * duration. + * + * @param {String} value the value to limit with + * @returns {Integer} the number of tries within the current window + */ async get(value) { const key = this.key(value); return client().get(key); } + /** + * test will increment the number of tries, reset the window length and + * will throw an error if the number of tries exceed the maximum for the + * window duration. + * + * @param {String} value the value to limit with + * @returns {Promise} resolves to the number of tries, or throws an error + */ async test(value) { const key = this.key(value); From 812483931d3d1b4eec55b68e64f3f41cbb6b46c1 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 24 Oct 2017 13:13:44 -0300 Subject: [PATCH 25/40] ref --- .../src/routes/Community/containers/People.js | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/client/coral-admin/src/routes/Community/containers/People.js b/client/coral-admin/src/routes/Community/containers/People.js index 5cd6f6e9c..561e0fcfb 100644 --- a/client/coral-admin/src/routes/Community/containers/People.js +++ b/client/coral-admin/src/routes/Community/containers/People.js @@ -21,14 +21,25 @@ class PeopleContainer extends React.Component { timer: null }; + fetchUsers = (query = {}) => { + const {community} = this.props; + + this.props.fetchUsers({ + value: this.state.searchValue, + field: community.fieldPeople, + asc: community.ascPeople, + ...query + }); + } + componentWillMount() { - this.props.fetchUsers(); + this.fetchUsers(); } onKeyDownHandler = (e) => { if (e.key === 'Enter') { e.preventDefault(); - this.search(); + this.fetchUsers(); } } @@ -38,9 +49,9 @@ class PeopleContainer extends React.Component { this.setState((prevState) => { prevState.searchValue = value; clearTimeout(prevState.timer); - const fetchAccounts = this.props.fetchUsers; + prevState.timer = setTimeout(() => { - fetchAccounts({value}); + this.fetchUsers({value}); }, 350); return prevState; }); @@ -48,25 +59,15 @@ class PeopleContainer extends React.Component { onHeaderClickHandler = (sort) => { this.props.updateSorting(sort); - this.search(); + this.fetchUsers(); } onNewPageHandler = ({selected}) => { const page = selected + 1; this.props.newPage(page); - this.search({page}); + this.fetchUsers({page}); } - search(query = {}) { - const {community} = this.props; - - this.props.fetchUsers({ - value: this.state.searchValue, - field: community.fieldPeople, - asc: community.ascPeople, - ...query - }); - } render() { return Date: Tue, 24 Oct 2017 18:47:55 +0200 Subject: [PATCH 26/40] Simplified Spinner for IE and Edge for Spinner (using css hacks) --- client/coral-ui/components/Spinner.css | 65 +++++++++----------------- 1 file changed, 23 insertions(+), 42 deletions(-) diff --git a/client/coral-ui/components/Spinner.css b/client/coral-ui/components/Spinner.css index 00b7f93a5..da09758f1 100644 --- a/client/coral-ui/components/Spinner.css +++ b/client/coral-ui/components/Spinner.css @@ -4,49 +4,28 @@ } .spinner { - -webkit-animation: rotator 1.4s linear infinite; animation: rotator 1.4s linear infinite; } -@-webkit-keyframes rotator { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(270deg); - transform: rotate(270deg); - } -} - @keyframes rotator { 0% { - -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { - -webkit-transform: rotate(270deg); transform: rotate(270deg); } } + + + .path { + stroke: #f67150; stroke-dasharray: 187; stroke-dashoffset: 0; - -webkit-transform-origin: center; transform-origin: center; - -webkit-animation: dash 1.4s ease-in-out infinite, colors 5.6s ease-in-out infinite; animation: dash 1.4s ease-in-out infinite, colors 5.6s ease-in-out infinite; } -@-webkit-keyframes colors { - 0% { - stroke: #f67150; - } - 100% { - stroke: #f6a47e; - } -} - @keyframes colors { 0% { stroke: #f67150; @@ -55,33 +34,35 @@ stroke: #f6a47e; } } -@-webkit-keyframes dash { - 0% { - stroke-dashoffset: 187; - } - 50% { - stroke-dashoffset: 46.75; - -webkit-transform: rotate(135deg); - transform: rotate(135deg); - } - 100% { - stroke-dashoffset: 187; - -webkit-transform: rotate(450deg); - transform: rotate(450deg); - } -} + @keyframes dash { 0% { stroke-dashoffset: 187; } 50% { stroke-dashoffset: 46.75; - -webkit-transform: rotate(135deg); transform: rotate(135deg); } 100% { stroke-dashoffset: 187; - -webkit-transform: rotate(450deg); transform: rotate(450deg); } } + +@keyframes fullRotator { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* Hack for IE and Edge as they don't support css animations on SVG elements. */ +_:-ms-lang(x), .path { + stroke-dasharray: 160; +} + +_:-ms-lang(x), .spinner { + animation: fullRotator 1.4s linear infinite; +} From 1dec169520b642b986be753d69453d6da26005ed Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 24 Oct 2017 11:03:15 -0600 Subject: [PATCH 27/40] added port parsing for smtp --- config.js | 2 +- services/mailer.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config.js b/config.js index 368fcf0e3..4601683c1 100644 --- a/config.js +++ b/config.js @@ -163,7 +163,7 @@ const CONFIG = { SMTP_FROM_ADDRESS: process.env.TALK_SMTP_FROM_ADDRESS, SMTP_HOST: process.env.TALK_SMTP_HOST, SMTP_PASSWORD: process.env.TALK_SMTP_PASSWORD, - SMTP_PORT: process.env.TALK_SMTP_PORT, + SMTP_PORT: process.env.TALK_SMTP_PORT ? parseInt(process.env.TALK_SMTP_PORT) : undefined, SMTP_USERNAME: process.env.TALK_SMTP_USERNAME, //------------------------------------------------------------------------------ diff --git a/services/mailer.js b/services/mailer.js index 8d650726d..f04a6dc92 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -64,7 +64,11 @@ const options = { }; if (SMTP_PORT) { - options.port = SMTP_PORT; + try { + options.port = parseInt(SMTP_PORT); + } catch (e) { + throw new Error('TALK_SMTP_PORT is not an integer'); + } } else { options.port = 25; } From 27eb159a75dc85807c464cd28d51a61f4cf329de Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 25 Oct 2017 13:04:41 +0200 Subject: [PATCH 28/40] Add special popup handling for ie 11 --- client/coral-framework/components/Popup.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/coral-framework/components/Popup.js b/client/coral-framework/components/Popup.js index 33041442f..41a37237a 100644 --- a/client/coral-framework/components/Popup.js +++ b/client/coral-framework/components/Popup.js @@ -22,6 +22,9 @@ export default class Popup extends Component { ); this.setCallbacks(); + + // For some reasons IE needs a timeout before setting the callbacks... + setTimeout(() => this.setCallbacks(), 1000); } setCallbacks() { From 16bf8871614574e5c747982a415f75b3f1d854ff Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 25 Oct 2017 13:06:00 +0200 Subject: [PATCH 29/40] Add ie to ci testing --- scripts/e2e-ci.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/e2e-ci.sh b/scripts/e2e-ci.sh index 1e4f45691..f1376abcb 100755 --- a/scripts/e2e-ci.sh +++ b/scripts/e2e-ci.sh @@ -21,9 +21,7 @@ if [[ "${CIRCLE_BRANCH}" == "master" ]]; then # Test using browserstack. browserstack chrome browserstack firefox - - # temporarily turn off ci, please fix https://www.pivotaltracker.com/story/show/152144406. - # browserstack ie + browserstack ie # Safari >= 8 has issues connecting to browserstack-local. Safari < 8 is too old. # browserstack safari From 823bf18335af4463dd7ac5ab22f65aa94888bca6 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 25 Oct 2017 14:19:13 +0200 Subject: [PATCH 30/40] Add edge to ci --- scripts/e2e-ci.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/e2e-ci.sh b/scripts/e2e-ci.sh index f1376abcb..e0e94b4bf 100755 --- a/scripts/e2e-ci.sh +++ b/scripts/e2e-ci.sh @@ -22,13 +22,11 @@ if [[ "${CIRCLE_BRANCH}" == "master" ]]; then browserstack chrome browserstack firefox browserstack ie + browserstack edge # Safari >= 8 has issues connecting to browserstack-local. Safari < 8 is too old. # browserstack safari - # Edge 14 & 15 randomly fails when switching from the login popup back to the main window. - # browserstack edge - exit $exitCode else # When browserstack is not available test locally using chrome headless. From 833922f0aaf15bbe4779da46c6884e6a4212e5d4 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 25 Oct 2017 09:35:08 -0300 Subject: [PATCH 31/40] Missing reference to the dropdownContainer class --- .../src/routes/Moderation/components/ViewOptions.css | 2 +- .../src/routes/Moderation/components/ViewOptions.js | 1 + client/coral-ui/components/Dropdown.js | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/client/coral-admin/src/routes/Moderation/components/ViewOptions.css b/client/coral-admin/src/routes/Moderation/components/ViewOptions.css index e5dfc8c01..3e5aae32d 100644 --- a/client/coral-admin/src/routes/Moderation/components/ViewOptions.css +++ b/client/coral-admin/src/routes/Moderation/components/ViewOptions.css @@ -53,7 +53,7 @@ list-style: none; } -.dropdow { +.dropdownContainer { position: relative; margin-top: 5px; diff --git a/client/coral-admin/src/routes/Moderation/components/ViewOptions.js b/client/coral-admin/src/routes/Moderation/components/ViewOptions.js index c46b7f869..a65af66a6 100644 --- a/client/coral-admin/src/routes/Moderation/components/ViewOptions.js +++ b/client/coral-admin/src/routes/Moderation/components/ViewOptions.js @@ -23,6 +23,7 @@ class ViewOptions extends React.Component {
  • Sort Comments -
    +
    Date: Wed, 25 Oct 2017 16:47:51 +0200 Subject: [PATCH 32/40] Fix ci --- nightwatch-browserstack.conf.js | 2 +- nightwatch.conf.js | 2 +- yarn.lock | 212 +++++++------------------------- 3 files changed, 46 insertions(+), 170 deletions(-) diff --git a/nightwatch-browserstack.conf.js b/nightwatch-browserstack.conf.js index 604691c81..9e8e334c5 100644 --- a/nightwatch-browserstack.conf.js +++ b/nightwatch-browserstack.conf.js @@ -31,7 +31,7 @@ const nightwatch_config = { chrome: { desiredCapabilities: { browser: 'chrome', - browser_version: '60', + browser_version: '62', }, }, firefox: { diff --git a/nightwatch.conf.js b/nightwatch.conf.js index fddbce229..f843b881a 100644 --- a/nightwatch.conf.js +++ b/nightwatch.conf.js @@ -43,7 +43,7 @@ module.exports = { 'chrome-headless': { desiredCapabilities: { chromeOptions : { - args: ['--headless', '--disable-gpu'], + args: ['--headless', '--disable-gpu', 'window-size=1280,800'], }, }, }, diff --git a/yarn.lock b/yarn.lock index dc23d05f8..6ea60fc82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -72,11 +72,7 @@ abab@^1.0.0, abab@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - -abbrev@1.0.x: +abbrev@1, abbrev@1.0.x: version "1.0.9" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" @@ -403,18 +399,12 @@ async@2.1.4: dependencies: lodash "^4.14.0" -async@2.4.1: +async@2.4.1, async@^2.1.2, async@^2.1.4: version "2.4.1" resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7" dependencies: lodash "^4.14.0" -async@^2.1.2, async@^2.1.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" - dependencies: - lodash "^4.14.0" - async@~0.9.0: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" @@ -1933,14 +1923,10 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" -cookiejar@2.0.x: +cookiejar@2.0.x, cookiejar@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.0.6.tgz#0abf356ad00d1c5a219d88d44518046dd026acfe" -cookiejar@^2.0.6: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a" - copy-webpack-plugin@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.1.1.tgz#53ae69e04955ebfa9fda411f54cbb968531d71fd" @@ -2375,14 +2361,10 @@ diff@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" -diff@3.2.0: +diff@3.2.0, diff@^3.1.0, diff@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" -diff@^3.1.0, diff@^3.2.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" - diffie-hellman@^5.0.0: version "5.0.2" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" @@ -2425,40 +2407,27 @@ domain-browser@^1.1.1: version "1.1.7" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" -domelementtype@1, domelementtype@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" - -domelementtype@~1.1.1: +domelementtype@1, domelementtype@~1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" -domhandler@2.3: +domelementtype@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" + +domhandler@2.3, domhandler@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738" dependencies: domelementtype "1" -domhandler@^2.3.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259" - dependencies: - domelementtype "1" - -domutils@1.5, domutils@1.5.1: +domutils@1.5, domutils@1.5.1, domutils@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" dependencies: dom-serializer "0" domelementtype "1" -domutils@^1.5.1: - version "1.6.2" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" - dependencies: - dom-serializer "0" - domelementtype "1" - dont-sniff-mimetype@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz#5932890dc9f4e2f19e5eb02a20026e5e5efc8f58" @@ -2890,7 +2859,7 @@ exports-loader@^0.6.4: loader-utils "^1.0.2" source-map "0.5.x" -express@4.16.0: +express@4.16.0, express@^4.12.2: version "4.16.0" resolved "https://registry.yarnpkg.com/express/-/express-4.16.0.tgz#b519638e4eb58e7178c81b498ef22f798cb2e255" dependencies: @@ -2925,41 +2894,6 @@ express@4.16.0: utils-merge "1.0.1" vary "~1.1.2" -express@^4.12.2: - version "4.16.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" - dependencies: - accepts "~1.3.4" - array-flatten "1.1.1" - body-parser "1.18.2" - content-disposition "0.5.2" - content-type "~1.0.4" - cookie "0.3.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.1" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.1.0" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.2" - path-to-regexp "0.1.7" - proxy-addr "~2.0.2" - qs "6.5.1" - range-parser "~1.2.0" - safe-buffer "5.1.1" - send "0.16.1" - serve-static "1.13.1" - setprototypeof "1.1.0" - statuses "~1.3.1" - type-is "~1.6.15" - utils-merge "1.0.1" - vary "~1.1.2" - extend@3, extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -3008,7 +2942,7 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.1, fbjs@^0.8.16, fbjs@^0.8.9: +fbjs@^0.8.1, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.9: version "0.8.16" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" dependencies: @@ -4047,7 +3981,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" @@ -4185,7 +4119,7 @@ ip@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ip/-/ip-1.0.1.tgz#c7e356cdea225ae71b36d70f2e71a92ba4e42590" -ip@^1.1.2, ip@^1.1.4: +ip@^1.1.2: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" @@ -5515,9 +5449,9 @@ lowercase-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" -lru-cache@^2.5.0: - version "2.7.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" +lru-cache@^2.5.0, lru-cache@~2.6.5: + version "2.6.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5" lru-cache@^4.0.1: version "4.1.1" @@ -5526,10 +5460,6 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" -lru-cache@~2.6.5: - version "2.6.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5" - macaddress@^0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" @@ -5707,7 +5637,7 @@ minimatch@3.0.3: dependencies: brace-expansion "^1.0.0" -minimist@0.0.8: +minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -5715,10 +5645,6 @@ minimist@^1.1.1, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -7115,7 +7041,7 @@ promised-io@*: version "0.3.5" resolved "https://registry.yarnpkg.com/promised-io/-/promised-io-0.3.5.tgz#4ad217bb3658bcaae9946b17a8668ecd851e1356" -prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8: +prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0: version "15.6.0" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" dependencies: @@ -7387,6 +7313,14 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-addons-create-fragment@^15.0.0: + version "15.6.2" + resolved "https://registry.yarnpkg.com/react-addons-create-fragment/-/react-addons-create-fragment-15.6.2.tgz#a394de7c2c7becd6b5475ba1b97ac472ce7c74f8" + dependencies: + fbjs "^0.8.4" + loose-envify "^1.3.1" + object-assign "^4.1.0" + react-apollo@^1.4.12: version "1.4.16" resolved "https://registry.yarnpkg.com/react-apollo/-/react-apollo-1.4.16.tgz#62a623458b67a174ff8ef25f64e7b42531518e19" @@ -7435,6 +7369,14 @@ react-mdl@^1.7.1, react-mdl@^1.7.2: lodash.isequal "^4.4.0" prop-types "^15.5.0" +react-paginate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/react-paginate/-/react-paginate-5.0.0.tgz#b5c12191ea81adc6d4d1b339b805e81841eaa8ea" + dependencies: + classnames "^2.2.5" + prop-types "^15.6.0" + react-addons-create-fragment "^15.0.0" + react-recaptcha@^2.2.6: version "2.3.5" resolved "https://registry.yarnpkg.com/react-recaptcha/-/react-recaptcha-2.3.5.tgz#a5db337125bb00fb13c2fa2e4ebfbe8b0cd06bb7" @@ -7466,20 +7408,13 @@ react-tagsinput@^3.17.0: version "3.18.0" resolved "https://registry.yarnpkg.com/react-tagsinput/-/react-tagsinput-3.18.0.tgz#40e036fc0f4c3d6b4689858189ab02926717a818" -react-test-renderer@15.5: +react-test-renderer@15.5, react-test-renderer@^15.5.0: version "15.5.4" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.5.4.tgz#d4ebb23f613d685ea8f5390109c2d20fbf7c83bc" dependencies: fbjs "^0.8.9" object-assign "^4.1.0" -react-test-renderer@^15.5.0: - version "15.6.2" - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.6.2.tgz#d0333434fc2c438092696ca770da5ed48037efa8" - dependencies: - fbjs "^0.8.9" - object-assign "^4.1.0" - react-test-renderer@^16.0.0-0: version "16.0.0" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.0.0.tgz#9fe7b8308f2f71f29fc356d4102086f131c9cb15" @@ -7550,7 +7485,7 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -readable-stream@1.1: +readable-stream@1.1, readable-stream@1.1.x: version "1.1.13" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e" dependencies: @@ -7559,28 +7494,7 @@ readable-stream@1.1: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@1.1.x: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - -readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6: - version "2.3.3" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - safe-buffer "~5.1.1" - string_decoder "~1.0.3" - util-deprecate "~1.0.1" - -readable-stream@2.2.7: +readable-stream@2, readable-stream@2.2.7, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6: version "2.2.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.7.tgz#07057acbe2467b22042d36f98c5ad507054e95b1" dependencies: @@ -7991,11 +7905,11 @@ rx@^2.4.3: version "2.5.3" resolved "https://registry.yarnpkg.com/rx/-/rx-2.5.3.tgz#21adc7d80f02002af50dae97fd9dbf248755f566" -safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" -samsam@1.1.2: +samsam@1.1.2, samsam@~1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" @@ -8003,10 +7917,6 @@ samsam@1.x, samsam@^1.1.3: version "1.3.0" resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" -samsam@~1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621" - sane@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/sane/-/sane-2.2.0.tgz#d6d2e2fcab00e3d283c93b912b7c3a20846f1d56" @@ -8097,24 +8007,6 @@ send@0.16.0: range-parser "~1.2.0" statuses "~1.3.1" -send@0.16.1: - version "0.16.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3" - dependencies: - debug "2.6.9" - depd "~1.1.1" - destroy "~1.0.4" - encodeurl "~1.0.1" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.6.2" - mime "1.4.1" - ms "2.0.0" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.3.1" - serve-static@1.13.0: version "1.13.0" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.0.tgz#810c91db800e94ba287eae6b4e06caab9fdc16f1" @@ -8124,15 +8016,6 @@ serve-static@1.13.0: parseurl "~1.3.2" send "0.16.0" -serve-static@1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719" - dependencies: - encodeurl "~1.0.1" - escape-html "~1.0.3" - parseurl "~1.3.2" - send "0.16.1" - set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -8261,7 +8144,7 @@ sliced@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" -smart-buffer@^1.0.13, smart-buffer@^1.0.4: +smart-buffer@^1.0.4: version "1.1.15" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16" @@ -8302,20 +8185,13 @@ socks-proxy-agent@2: extend "3" socks "~1.1.5" -socks@1.1.9: +socks@1.1.9, socks@~1.1.5: version "1.1.9" resolved "https://registry.yarnpkg.com/socks/-/socks-1.1.9.tgz#628d7e4d04912435445ac0b6e459376cb3e6d691" dependencies: ip "^1.1.2" smart-buffer "^1.0.4" -socks@~1.1.5: - version "1.1.10" - resolved "https://registry.yarnpkg.com/socks/-/socks-1.1.10.tgz#5b8b7fc7c8f341c53ed056e929b7bf4de8ba7b5a" - dependencies: - ip "^1.1.4" - smart-buffer "^1.0.13" - sort-keys@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" @@ -8463,7 +8339,7 @@ string_decoder@^0.10.25, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" -string_decoder@~1.0.0, string_decoder@~1.0.3: +string_decoder@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" dependencies: From df079d29bdba34f5051261246c4df44ac42789ff Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 25 Oct 2017 17:38:31 +0200 Subject: [PATCH 33/40] Set current page declaratively for People --- client/coral-admin/src/actions/community.js | 7 +++-- client/coral-admin/src/constants/community.js | 30 ++++++++++--------- client/coral-admin/src/reducers/community.js | 6 ++++ .../src/routes/Community/components/People.js | 6 ++-- .../src/routes/Community/containers/People.js | 16 +++++----- client/coral-ui/components/Paginate.js | 4 ++- 6 files changed, 40 insertions(+), 29 deletions(-) diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index b7102b8ff..89427f7bb 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -5,7 +5,7 @@ import { FETCH_USERS_SUCCESS, FETCH_USERS_FAILURE, SORT_UPDATE, - COMMENTERS_NEW_PAGE, + SET_PAGE, SET_ROLE, SET_COMMENTER_STATUS, SHOW_BANUSER_DIALOG, @@ -45,8 +45,9 @@ export const updateSorting = (sort) => ({ sort }); -export const newPage = () => ({ - type: COMMENTERS_NEW_PAGE +export const setPage = (page) => ({ + type: SET_PAGE, + page, }); export const setRole = (id, role) => (dispatch, _, {rest}) => { diff --git a/client/coral-admin/src/constants/community.js b/client/coral-admin/src/constants/community.js index dc11572ed..6e8a82bfe 100644 --- a/client/coral-admin/src/constants/community.js +++ b/client/coral-admin/src/constants/community.js @@ -1,18 +1,20 @@ -export const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST'; -export const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS'; -export const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE'; +const prefix = 'COMMUNITY'; -export const SORT_UPDATE = 'SORT_UPDATE'; -export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE'; -export const SET_ROLE = 'SET_ROLE'; -export const SET_COMMENTER_STATUS = 'SET_COMMENTER_STATUS'; +export const FETCH_USERS_REQUEST = `${prefix}_FETCH_USERS_REQUEST`; +export const FETCH_USERS_SUCCESS = `${prefix}_FETCH_USERS_SUCCESS`; +export const FETCH_USERS_FAILURE = `${prefix}_FETCH_USERS_FAILURE`; -export const FETCH_FLAGGED_COMMENTERS_REQUEST = 'FETCH_FLAGGED_COMMENTERS_REQUEST'; -export const FETCH_FLAGGED_COMMENTERS_SUCCESS = 'FETCH_FLAGGED_COMMENTERS_SUCCESS'; -export const FETCH_FLAGGED_COMMENTERS_FAILURE = 'FETCH_FLAGGED_COMMENTERS_FAILURE'; +export const SORT_UPDATE = `${prefix}_SORT_UPDATE`; +export const SET_PAGE = `${prefix}_SET_PAGE`; +export const SET_ROLE = `${prefix}_SET_ROLE`; +export const SET_COMMENTER_STATUS = `${prefix}_SET_COMMENTER_STATUS`; -export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG'; -export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG'; +export const FETCH_FLAGGED_COMMENTERS_REQUEST = `${prefix}_FETCH_FLAGGED_COMMENTERS_REQUEST`; +export const FETCH_FLAGGED_COMMENTERS_SUCCESS = `${prefix}_FETCH_FLAGGED_COMMENTERS_SUCCESS`; +export const FETCH_FLAGGED_COMMENTERS_FAILURE = `${prefix}_FETCH_FLAGGED_COMMENTERS_FAILURE`; -export const SHOW_REJECT_USERNAME_DIALOG = 'SHOW_REJECT_USERNAME_DIALOG'; -export const HIDE_REJECT_USERNAME_DIALOG = 'HIDE_REJECT_USERNAME_DIALOG'; +export const SHOW_BANUSER_DIALOG = `${prefix}_SHOW_BANUSER_DIALOG`; +export const HIDE_BANUSER_DIALOG = `${prefix}_HIDE_BANUSER_DIALOG`; + +export const SHOW_REJECT_USERNAME_DIALOG = `${prefix}_SHOW_REJECT_USERNAME_DIALOG`; +export const HIDE_REJECT_USERNAME_DIALOG = `${prefix}_HIDE_REJECT_USERNAME_DIALOG`; diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js index 31fb6aa17..cb71634cc 100644 --- a/client/coral-admin/src/reducers/community.js +++ b/client/coral-admin/src/reducers/community.js @@ -3,6 +3,7 @@ import { FETCH_USERS_SUCCESS, FETCH_USERS_FAILURE, SORT_UPDATE, + SET_PAGE, SET_ROLE, SET_COMMENTER_STATUS, SHOW_BANUSER_DIALOG, @@ -52,6 +53,11 @@ export default function community (state = initialState, action) { users, // Sets to normal array }; } + case SET_PAGE: + return { + ...state, + pagePeople: action.page, + }; case SET_ROLE : { const commenters = state.users; const idx = commenters.findIndex((el) => el.id === action.id); diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js index 9626dd53f..664173cb0 100644 --- a/client/coral-admin/src/routes/Community/components/People.js +++ b/client/coral-admin/src/routes/Community/components/People.js @@ -13,7 +13,7 @@ const People = (props) => { searchValue, onSearchChange, onHeaderClickHandler, - onNewPageHandler, + onPageChange, totalPages, setRole, setCommenterStatus, @@ -47,7 +47,7 @@ const People = (props) => { setCommenterStatus={setCommenterStatus} onHeaderClickHandler={onHeaderClickHandler} pageCount={totalPages} - onPageChange={onNewPageHandler} + onPageChange={onPageChange} /> : {t('community.no_results')} } @@ -62,7 +62,7 @@ People.propTypes = { searchValue: PropTypes.string, onSearchChange: PropTypes.func, totalPages: PropTypes.number, - onNewPageHandler: PropTypes.func, + onPageChange: PropTypes.func, setCommenterStatus: PropTypes.func.isRequired, setRole: PropTypes.func.isRequired, viewUserDetail: PropTypes.func.isRequired, diff --git a/client/coral-admin/src/routes/Community/containers/People.js b/client/coral-admin/src/routes/Community/containers/People.js index 561e0fcfb..5780529e8 100644 --- a/client/coral-admin/src/routes/Community/containers/People.js +++ b/client/coral-admin/src/routes/Community/containers/People.js @@ -9,7 +9,7 @@ import {viewUserDetail} from '../../../actions/userDetail'; import { fetchUsers, updateSorting, - newPage, + setPage, hideRejectUsernameDialog, setCommenterStatus, setRole, @@ -23,7 +23,7 @@ class PeopleContainer extends React.Component { fetchUsers = (query = {}) => { const {community} = this.props; - + this.props.fetchUsers({ value: this.state.searchValue, field: community.fieldPeople, @@ -62,19 +62,19 @@ class PeopleContainer extends React.Component { this.fetchUsers(); } - onNewPageHandler = ({selected}) => { + onPageChange = ({selected}) => { const page = selected + 1; - this.props.newPage(page); + this.props.setPage(page); this.fetchUsers({page}); } render() { - return bindActionCreators({ - newPage, + setPage, fetchUsers, updateSorting, hideRejectUsernameDialog, diff --git a/client/coral-ui/components/Paginate.js b/client/coral-ui/components/Paginate.js index 3a404db69..df19450c3 100644 --- a/client/coral-ui/components/Paginate.js +++ b/client/coral-ui/components/Paginate.js @@ -4,9 +4,10 @@ import ReactPaginate from 'react-paginate'; import styles from './Paginate.css'; import Icon from './Icon'; -const Paginate = ({pageCount, onPageChange}) => ( +const Paginate = ({pageCount, page, onPageChange}) => ( ( ); Paginate.propTypes = { + page: PropTypes.number.isRequired, pageCount: PropTypes.number.isRequired, onPageChange: PropTypes.func.isRequired, }; From 23da613f2b30466cd4932da2771db118d510678c Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 25 Oct 2017 18:02:18 +0200 Subject: [PATCH 34/40] Declaratively set page --- .../src/actions/{assets.js => stories.js} | 9 ++++++++- client/coral-admin/src/constants/assets.js | 9 --------- client/coral-admin/src/constants/stories.js | 13 +++++++++++++ client/coral-admin/src/reducers/index.js | 4 ++-- .../src/reducers/{assets.js => stories.js} | 7 ++++++- .../src/routes/Community/components/People.js | 3 +++ .../src/routes/Community/components/Table.js | 10 ++++++---- .../src/routes/Community/containers/People.js | 1 + .../src/routes/Stories/components/Stories.js | 7 +++++-- .../src/routes/Stories/containers/Stories.js | 7 +++++-- 10 files changed, 49 insertions(+), 21 deletions(-) rename client/coral-admin/src/actions/{assets.js => stories.js} (93%) delete mode 100644 client/coral-admin/src/constants/assets.js create mode 100644 client/coral-admin/src/constants/stories.js rename client/coral-admin/src/reducers/{assets.js => stories.js} (86%) diff --git a/client/coral-admin/src/actions/assets.js b/client/coral-admin/src/actions/stories.js similarity index 93% rename from client/coral-admin/src/actions/assets.js rename to client/coral-admin/src/actions/stories.js index d7c6d9448..536f478e1 100644 --- a/client/coral-admin/src/actions/assets.js +++ b/client/coral-admin/src/actions/stories.js @@ -4,11 +4,12 @@ import { FETCH_ASSETS_REQUEST, FETCH_ASSETS_SUCCESS, FETCH_ASSETS_FAILURE, + SET_PAGE, UPDATE_ASSET_STATE_REQUEST, UPDATE_ASSET_STATE_SUCCESS, UPDATE_ASSET_STATE_FAILURE, UPDATE_ASSETS -} from '../constants/assets'; +} from '../constants/stories'; import t from 'coral-framework/services/i18n'; @@ -52,3 +53,9 @@ export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => { export const updateAssets = (assets) => (dispatch) => { dispatch({type: UPDATE_ASSETS, assets}); }; + +export const setPage = (page) => ({ + type: SET_PAGE, + page, +}); + diff --git a/client/coral-admin/src/constants/assets.js b/client/coral-admin/src/constants/assets.js deleted file mode 100644 index 20ec0a9c3..000000000 --- a/client/coral-admin/src/constants/assets.js +++ /dev/null @@ -1,9 +0,0 @@ -export const FETCH_ASSETS_REQUEST = 'FETCH_ASSETS_REQUEST'; -export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS'; -export const FETCH_ASSETS_FAILURE = 'FETCH_ASSETS_FAILURE'; - -export const UPDATE_ASSET_STATE_REQUEST = 'UPDATE_ASSET_STATE_REQUEST'; -export const UPDATE_ASSET_STATE_SUCCESS = 'UPDATE_ASSET_STATE_SUCCESS'; -export const UPDATE_ASSET_STATE_FAILURE = 'UPDATE_ASSET_STATE_FAILURE'; - -export const UPDATE_ASSETS = 'UPDATE_ASSETS'; diff --git a/client/coral-admin/src/constants/stories.js b/client/coral-admin/src/constants/stories.js new file mode 100644 index 000000000..52a9f9943 --- /dev/null +++ b/client/coral-admin/src/constants/stories.js @@ -0,0 +1,13 @@ +const prefix = 'STORIES'; + +export const FETCH_ASSETS_REQUEST = `${prefix}_FETCH_ASSETS_REQUEST`; +export const FETCH_ASSETS_SUCCESS = `${prefix}_FETCH_ASSETS_SUCCESS`; +export const FETCH_ASSETS_FAILURE = `${prefix}_FETCH_ASSETS_FAILURE`; + +export const UPDATE_ASSET_STATE_REQUEST = `${prefix}_UPDATE_ASSET_STATE_REQUEST`; +export const UPDATE_ASSET_STATE_SUCCESS = `${prefix}_UPDATE_ASSET_STATE_SUCCESS`; +export const UPDATE_ASSET_STATE_FAILURE = `${prefix}_UPDATE_ASSET_STATE_FAILURE`; + +export const UPDATE_ASSETS = `${prefix}_UPDATE_ASSETS`; + +export const SET_PAGE = `${prefix}_SET_PAGE`; diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js index 372fedbae..f7bbf3a3b 100644 --- a/client/coral-admin/src/reducers/index.js +++ b/client/coral-admin/src/reducers/index.js @@ -1,5 +1,5 @@ import auth from './auth'; -import assets from './assets'; +import stories from './stories'; import dashboard from './dashboard'; import configure from './configure'; import community from './community'; @@ -17,7 +17,7 @@ export default { configure, suspendUserDialog, userDetail, - assets, + stories, community, moderation, install, diff --git a/client/coral-admin/src/reducers/assets.js b/client/coral-admin/src/reducers/stories.js similarity index 86% rename from client/coral-admin/src/reducers/assets.js rename to client/coral-admin/src/reducers/stories.js index b4ea8d21f..60c02bc99 100644 --- a/client/coral-admin/src/reducers/assets.js +++ b/client/coral-admin/src/reducers/stories.js @@ -1,4 +1,4 @@ -import * as actions from '../constants/assets'; +import * as actions from '../constants/stories'; import update from 'immutability-helper'; const initialState = { @@ -35,6 +35,11 @@ export default function assets (state = initialState, action) { return update(state, { assets: {$set: action.assets}, }); + case actions.SET_PAGE: + return { + ...state, + page: action.page, + }; default: return state; } diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js index 664173cb0..75d08429a 100644 --- a/client/coral-admin/src/routes/Community/components/People.js +++ b/client/coral-admin/src/routes/Community/components/People.js @@ -15,6 +15,7 @@ const People = (props) => { onHeaderClickHandler, onPageChange, totalPages, + page, setRole, setCommenterStatus, viewUserDetail, @@ -48,6 +49,7 @@ const People = (props) => { onHeaderClickHandler={onHeaderClickHandler} pageCount={totalPages} onPageChange={onPageChange} + page={page} /> : {t('community.no_results')} } @@ -59,6 +61,7 @@ const People = (props) => { People.propTypes = { onHeaderClickHandler: PropTypes.func, users: PropTypes.array, + page: PropTypes.number.isRequired, searchValue: PropTypes.string, onSearchChange: PropTypes.func, totalPages: PropTypes.number, diff --git a/client/coral-admin/src/routes/Community/components/Table.js b/client/coral-admin/src/routes/Community/components/Table.js index 8d650f165..22b02cfe4 100644 --- a/client/coral-admin/src/routes/Community/components/Table.js +++ b/client/coral-admin/src/routes/Community/components/Table.js @@ -24,7 +24,7 @@ const headers = [ } ]; -const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUserDetail, pageCount, onPageChange}) => ( +const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUserDetail, pageCount, page, onPageChange}) => (
    @@ -51,13 +51,13 @@ const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUs {row.created_at}
    - setCommenterStatus(row.id, status)}> + onChange={(status) => setCommenterStatus(row.id, status)}> + @@ -88,6 +89,7 @@ Table.propTypes = { setCommenterStatus: PropTypes.func.isRequired, viewUserDetail: PropTypes.func.isRequired, pageCount: PropTypes.number.isRequired, + page: PropTypes.number.isRequired, onPageChange: PropTypes.func.isRequired, }; diff --git a/client/coral-admin/src/routes/Community/containers/People.js b/client/coral-admin/src/routes/Community/containers/People.js index 5780529e8..1f52846c9 100644 --- a/client/coral-admin/src/routes/Community/containers/People.js +++ b/client/coral-admin/src/routes/Community/containers/People.js @@ -78,6 +78,7 @@ class PeopleContainer extends React.Component { totalPages={this.props.community.totalPagesPeople} setCommenterStatus={this.props.setCommenterStatus} setRole={this.props.setRole} + page={this.props.community.pagePeople} viewUserDetail={this.props.viewUserDetail} />; } diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index 73f5c60dc..3b2787aab 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -91,8 +91,9 @@ class Stories extends Component { ); } - onPageClick = ({selected}) => { + onPageChange = ({selected}) => { const page = selected + 1; + this.props.setPage(page); this.props.fetchAssets({page}); } @@ -157,7 +158,8 @@ class Stories extends Component { + page={assets.page - 1} + onPageChange={this.onPageChange} /> : {t('streams.empty_result')} } @@ -168,6 +170,7 @@ class Stories extends Component { Stories.propTypes = { assets: PropTypes.object, + setPage: PropTypes.func, fetchAssets: PropTypes.func, updateAssetState: PropTypes.func, }; diff --git a/client/coral-admin/src/routes/Stories/containers/Stories.js b/client/coral-admin/src/routes/Stories/containers/Stories.js index b940e46e6..28048f39d 100644 --- a/client/coral-admin/src/routes/Stories/containers/Stories.js +++ b/client/coral-admin/src/routes/Stories/containers/Stories.js @@ -1,12 +1,15 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; -import {fetchAssets, updateAssetState} from 'coral-admin/src/actions/assets'; +import {fetchAssets, updateAssetState, setPage} from 'coral-admin/src/actions/stories'; import Stories from '../components/Stories'; -const mapStateToProps = ({assets}) => ({assets}); +const mapStateToProps = ({stories}) => ({ + assets: stories, +}); const mapDispatchToProps = (dispatch) => bindActionCreators({ + setPage, fetchAssets, updateAssetState, }, dispatch); From 20b5525c48be493f9829f213763bae7029bb3971 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 25 Oct 2017 19:07:14 +0200 Subject: [PATCH 35/40] Refactor and fix query --- client/coral-admin/src/actions/stories.js | 12 +++ client/coral-admin/src/constants/stories.js | 2 + client/coral-admin/src/reducers/stories.js | 50 +++++++--- .../src/routes/Stories/components/Stories.js | 92 +++--------------- .../src/routes/Stories/containers/Stories.js | 94 ++++++++++++++++++- 5 files changed, 157 insertions(+), 93 deletions(-) diff --git a/client/coral-admin/src/actions/stories.js b/client/coral-admin/src/actions/stories.js index 536f478e1..2524b0c3c 100644 --- a/client/coral-admin/src/actions/stories.js +++ b/client/coral-admin/src/actions/stories.js @@ -5,6 +5,8 @@ import { FETCH_ASSETS_SUCCESS, FETCH_ASSETS_FAILURE, SET_PAGE, + SET_SEARCH_VALUE, + SET_CRITERIA, UPDATE_ASSET_STATE_REQUEST, UPDATE_ASSET_STATE_SUCCESS, UPDATE_ASSET_STATE_FAILURE, @@ -59,3 +61,13 @@ export const setPage = (page) => ({ page, }); +export const setSearchValue = (value) => ({ + type: SET_SEARCH_VALUE, + value, +}); + +export const setCriteria = (criteria) => ({ + type: SET_CRITERIA, + criteria, +}); + diff --git a/client/coral-admin/src/constants/stories.js b/client/coral-admin/src/constants/stories.js index 52a9f9943..d1994e19a 100644 --- a/client/coral-admin/src/constants/stories.js +++ b/client/coral-admin/src/constants/stories.js @@ -11,3 +11,5 @@ export const UPDATE_ASSET_STATE_FAILURE = `${prefix}_UPDATE_ASSET_STATE_FAILURE` export const UPDATE_ASSETS = `${prefix}_UPDATE_ASSETS`; export const SET_PAGE = `${prefix}_SET_PAGE`; +export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`; +export const SET_CRITERIA = `${prefix}_SET_CRITERIA`; diff --git a/client/coral-admin/src/reducers/stories.js b/client/coral-admin/src/reducers/stories.js index 60c02bc99..0d640e6c2 100644 --- a/client/coral-admin/src/reducers/stories.js +++ b/client/coral-admin/src/reducers/stories.js @@ -2,9 +2,16 @@ import * as actions from '../constants/stories'; import update from 'immutability-helper'; const initialState = { - byId: {}, - ids: [], - assets: [] + assets: { + byId: {}, + ids: [], + assets: [] + }, + searchValue: '', + criteria: { + sort: 'desc', + filter: 'all', + }, }; export default function assets (state = initialState, action) { @@ -16,30 +23,49 @@ export default function assets (state = initialState, action) { }, {}); return update(state, { - totalPages: {$set: action.totalPages}, - page: {$set: action.page}, - byId: {$set: assets}, - count: {$set: action.count}, - ids: {$set: Object.keys(assets)}, + assets: { + totalPages: {$set: action.totalPages}, + page: {$set: action.page}, + byId: {$set: assets}, + count: {$set: action.count}, + ids: {$set: Object.keys(assets)}, + }, }); } case actions.UPDATE_ASSET_STATE_REQUEST: return update(state, { - byId: { - [action.id]: { - closedAt: {$set: action.closedAt}, + assets: { + byId: { + [action.id]: { + closedAt: {$set: action.closedAt}, + }, }, }, }); case actions.UPDATE_ASSETS: return update(state, { - assets: {$set: action.assets}, + assets: { + assets: {$set: action.assets}, + }, }); case actions.SET_PAGE: return { ...state, page: action.page, }; + case actions.SET_SEARCH_VALUE: + return { + ...state, + searchValue: action.value, + }; + case actions.SET_CRITERIA: + return { + ...state, + criteria: { + ...state.criteria, + ...action.criteria, + }, + }; default: return state; } diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index 3b2787aab..cc99416bc 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -11,72 +11,11 @@ import EmptyCard from 'coral-admin/src/components/EmptyCard'; class Stories extends Component { - state = { - searchValue: '', - sort: 'desc', - filter: 'all', - statusMenus: {}, - timer: null, - } - - componentDidMount () { - this.fetchAssets(); - } - - onSettingChange = (setting) => (e) => { - const {searchValue} = this.state; - const criteria = {[setting]: e.target.value}; - - this.setState(criteria); - - this.props.fetchAssets({ - value: searchValue, - ...criteria, - }); - } - - onSearchChange = (e) => { - const {value} = e.target; - - this.setState((prevState) => { - prevState.searchValue = value; - clearTimeout(prevState.timer); - - prevState.timer = setTimeout(() => { - this.fetchAssets(); - }, 350); - return prevState; - }); - } - renderDate = (date) => { const d = new Date(date); return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`; } - fetchAssets = (query) => { - const {searchValue, sort, filter, limit} = this.state; - - this.props.fetchAssets({ - value: searchValue, - sort, - filter, - limit, - ...query - }); - }; - - onStatusChange = async (closeStream, id) => { - const {updateAssetState} = this.props; - - try { - updateAssetState(id, closeStream ? Date.now() : null); - this.fetchAssets(); - } catch(err) { - console.error(err); - } - } - renderTitle = (title, {id}) => {title} renderStatus = (closedAt, {id}) => { @@ -84,26 +23,19 @@ class Stories extends Component { return ( this.onStatusChange(value, id)}> + onChange={(value) => this.props.onStatusChange(value, id)}> ); } - onPageChange = ({selected}) => { - const page = selected + 1; - this.props.setPage(page); - this.props.fetchAssets({page}); - } - render () { - const {searchValue, sort, filter} = this.state; - const {assets} = this.props; + const {assets, searchValue, sort, filter, onSearchChange, onSettingChange, onPageChange} = this.props; const assetsIds = sortBy(assets.ids.map((id) => assets.byId[id]), 'publication_date'); - if (this.state.sort === 'desc') { + if (sort === 'desc') { assetsIds.reverse(); } @@ -116,7 +48,7 @@ class Stories extends Component { type='text' value={searchValue} className={styles.searchBoxInput} - onChange={this.onSearchChange} + onChange={onSearchChange} placeholder={t('streams.search')}/>
    {t('streams.filter_streams')}
    @@ -125,7 +57,7 @@ class Stories extends Component { name='status filter' value={filter} childContainer='div' - onChange={this.onSettingChange('filter')} + onChange={onSettingChange('filter')} className={styles.radioGroup} > {t('streams.all')} @@ -137,7 +69,7 @@ class Stories extends Component { name='sort by' value={sort} childContainer='div' - onChange={this.onSettingChange('sort')} + onChange={onSettingChange('sort')} className={styles.radioGroup} > {t('streams.newest')} @@ -159,7 +91,7 @@ class Stories extends Component { + onPageChange={onPageChange} /> : {t('streams.empty_result')} } @@ -170,9 +102,13 @@ class Stories extends Component { Stories.propTypes = { assets: PropTypes.object, - setPage: PropTypes.func, - fetchAssets: PropTypes.func, - updateAssetState: PropTypes.func, + searchValue: PropTypes.string, + sort: PropTypes.string, + filter: PropTypes.string, + onStatusChange: PropTypes.func.isRequired, + onSearchChange: PropTypes.func.isRequired, + onPageChange: PropTypes.func.isRequired, + onSettingChange: PropTypes.func.isRequired, }; export default Stories; diff --git a/client/coral-admin/src/routes/Stories/containers/Stories.js b/client/coral-admin/src/routes/Stories/containers/Stories.js index 28048f39d..806bfc395 100644 --- a/client/coral-admin/src/routes/Stories/containers/Stories.js +++ b/client/coral-admin/src/routes/Stories/containers/Stories.js @@ -1,18 +1,106 @@ +import React, {Component} from 'react'; import {connect} from 'react-redux'; +import PropTypes from 'prop-types'; import {bindActionCreators} from 'redux'; -import {fetchAssets, updateAssetState, setPage} from 'coral-admin/src/actions/stories'; +import {fetchAssets, updateAssetState, setPage, setSearchValue, setCriteria} from 'coral-admin/src/actions/stories'; import Stories from '../components/Stories'; +class StoriesContainer extends Component { + timer=null; + + componentDidMount () { + this.fetchAssets(); + } + + onSearchChange = (e) => { + const {value} = e.target; + + this.props.setSearchValue(value); + clearTimeout(this.timer); + this.timer = setTimeout(() => { + this.fetchAssets(); + }, 350); + } + + onSettingChange = (setting) => (e) => { + const criteria = {[setting]: e.target.value}; + this.props.setCriteria(criteria); + this.fetchAssets(criteria); + } + + fetchAssets = (query) => { + const {searchValue, sort, filter, limit} = this.props; + + this.props.fetchAssets({ + value: searchValue, + sort, + filter, + limit, + ...query + }); + }; + + onStatusChange = async (closeStream, id) => { + const {updateAssetState} = this.props; + + try { + updateAssetState(id, closeStream ? Date.now() : null); + this.fetchAssets(); + } catch(err) { + console.error(err); + } + } + + onPageChange = ({selected}) => { + const page = selected + 1; + this.props.setPage(page); + this.fetchAssets({page}); + } + + render () { + return ; + } +} + const mapStateToProps = ({stories}) => ({ - assets: stories, + assets: stories.assets, + searchValue: stories.searchValue, + sort: stories.criteria.sort, + filter: stories.criteria.filter, + limit: stories.criteria.limit, }); const mapDispatchToProps = (dispatch) => bindActionCreators({ setPage, + setCriteria, + setSearchValue, fetchAssets, updateAssetState, }, dispatch); -export default connect(mapStateToProps, mapDispatchToProps)(Stories); +StoriesContainer.propTypes = { + assets: PropTypes.object, + searchValue: PropTypes.string, + sort: PropTypes.string, + filter: PropTypes.string, + limit: PropTypes.number, + setPage: PropTypes.func.isRequired, + setCriteria: PropTypes.func.isRequired, + setSearchValue: PropTypes.func.isRequired, + fetchAssets: PropTypes.func.isRequired, + updateAssetState: PropTypes.func.isRequired, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(StoriesContainer); From e41c224e3d303cfc865d8fa9d59afeb50485d44a Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 25 Oct 2017 19:14:25 +0200 Subject: [PATCH 36/40] Remove unused --- client/coral-admin/src/routes/Stories/components/Stories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index cc99416bc..f8f553cab 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -79,7 +79,7 @@ class Stories extends Component { { assetsIds.length ?
    - + {t('streams.article')} {t('streams.pubdate')} From b4daf8fc98da50520e2114d29f81f3256f8a99b9 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 25 Oct 2017 19:19:59 +0200 Subject: [PATCH 37/40] Move searchValue to Redux --- client/coral-admin/src/actions/community.js | 6 +++++ client/coral-admin/src/constants/community.js | 2 ++ client/coral-admin/src/reducers/community.js | 7 +++++ .../src/routes/Community/containers/People.js | 26 ++++++++----------- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/client/coral-admin/src/actions/community.js b/client/coral-admin/src/actions/community.js index 89427f7bb..6a091d214 100644 --- a/client/coral-admin/src/actions/community.js +++ b/client/coral-admin/src/actions/community.js @@ -6,6 +6,7 @@ import { FETCH_USERS_FAILURE, SORT_UPDATE, SET_PAGE, + SET_SEARCH_VALUE, SET_ROLE, SET_COMMENTER_STATUS, SHOW_BANUSER_DIALOG, @@ -50,6 +51,11 @@ export const setPage = (page) => ({ page, }); +export const setSearchValue = (value) => ({ + type: SET_SEARCH_VALUE, + value, +}); + export const setRole = (id, role) => (dispatch, _, {rest}) => { return rest(`/users/${id}/role`, {method: 'POST', body: {role}}) .then(() => { diff --git a/client/coral-admin/src/constants/community.js b/client/coral-admin/src/constants/community.js index 6e8a82bfe..eea946efb 100644 --- a/client/coral-admin/src/constants/community.js +++ b/client/coral-admin/src/constants/community.js @@ -18,3 +18,5 @@ export const HIDE_BANUSER_DIALOG = `${prefix}_HIDE_BANUSER_DIALOG`; export const SHOW_REJECT_USERNAME_DIALOG = `${prefix}_SHOW_REJECT_USERNAME_DIALOG`; export const HIDE_REJECT_USERNAME_DIALOG = `${prefix}_HIDE_REJECT_USERNAME_DIALOG`; + +export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`; diff --git a/client/coral-admin/src/reducers/community.js b/client/coral-admin/src/reducers/community.js index cb71634cc..170804b60 100644 --- a/client/coral-admin/src/reducers/community.js +++ b/client/coral-admin/src/reducers/community.js @@ -4,6 +4,7 @@ import { FETCH_USERS_FAILURE, SORT_UPDATE, SET_PAGE, + SET_SEARCH_VALUE, SET_ROLE, SET_COMMENTER_STATUS, SHOW_BANUSER_DIALOG, @@ -17,6 +18,7 @@ const initialState = { isFetchingPeople: false, errorPeople: '', users: [], + searchValue: '', fieldPeople: 'created_at', ascPeople: false, totalPagesPeople: 0, @@ -107,6 +109,11 @@ export default function community (state = initialState, action) { user: action.user, rejectUsernameDialog: true }; + case SET_SEARCH_VALUE: + return { + ...state, + searchValue: action.value, + }; default : return state; } diff --git a/client/coral-admin/src/routes/Community/containers/People.js b/client/coral-admin/src/routes/Community/containers/People.js index 1f52846c9..063eaef2f 100644 --- a/client/coral-admin/src/routes/Community/containers/People.js +++ b/client/coral-admin/src/routes/Community/containers/People.js @@ -13,19 +13,17 @@ import { hideRejectUsernameDialog, setCommenterStatus, setRole, + setSearchValue, } from '../../../actions/community'; class PeopleContainer extends React.Component { - state = { - searchValue: '', - timer: null - }; + timer=null; fetchUsers = (query = {}) => { const {community} = this.props; this.props.fetchUsers({ - value: this.state.searchValue, + value: community.searchValue, field: community.fieldPeople, asc: community.ascPeople, ...query @@ -46,15 +44,11 @@ class PeopleContainer extends React.Component { onSearchChange = (e) => { const value = e.target.value; - this.setState((prevState) => { - prevState.searchValue = value; - clearTimeout(prevState.timer); - - prevState.timer = setTimeout(() => { - this.fetchUsers({value}); - }, 350); - return prevState; - }); + this.props.setSearchValue(value); + clearTimeout(this.timer); + this.timer = setTimeout(() => { + this.fetchUsers({value}); + }, 350); } onHeaderClickHandler = (sort) => { @@ -71,7 +65,7 @@ class PeopleContainer extends React.Component { render() { return setCommenterStatus, setRole, viewUserDetail, + setSearchValue, }, dispatch); export default connect(null, mapDispatchToProps)(PeopleContainer); From d472223ee3ed9084ae2d01412350c14f3caf95de Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 25 Oct 2017 19:32:47 +0200 Subject: [PATCH 38/40] Fix/Implement sorting for stories --- client/coral-admin/src/reducers/stories.js | 2 +- .../src/routes/Stories/components/Stories.js | 24 +++++++------------ .../src/routes/Stories/containers/Stories.js | 10 ++++---- routes/api/assets/index.js | 2 +- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/client/coral-admin/src/reducers/stories.js b/client/coral-admin/src/reducers/stories.js index 0d640e6c2..271ec8cfe 100644 --- a/client/coral-admin/src/reducers/stories.js +++ b/client/coral-admin/src/reducers/stories.js @@ -9,7 +9,7 @@ const initialState = { }, searchValue: '', criteria: { - sort: 'desc', + asc: 'false', filter: 'all', }, }; diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js index f8f553cab..866defde4 100644 --- a/client/coral-admin/src/routes/Stories/components/Stories.js +++ b/client/coral-admin/src/routes/Stories/components/Stories.js @@ -2,7 +2,6 @@ import React, {Component} from 'react'; import cn from 'classnames'; import {Link} from 'react-router'; import PropTypes from 'prop-types'; -import sortBy from 'lodash/sortBy'; import {Dropdown, Option, Paginate, Icon} from 'coral-ui'; import {DataTable, TableHeader, RadioGroup, Radio} from 'react-mdl'; import t from 'coral-framework/services/i18n'; @@ -31,13 +30,8 @@ class Stories extends Component { } render () { - const {assets, searchValue, sort, filter, onSearchChange, onSettingChange, onPageChange} = this.props; - - const assetsIds = sortBy(assets.ids.map((id) => assets.byId[id]), 'publication_date'); - - if (sort === 'desc') { - assetsIds.reverse(); - } + const {assets, searchValue, filter, onSearchChange, onSettingChange, onPageChange, asc} = this.props; + const rows = assets.ids.map((id) => assets.byId[id]); return (
    @@ -67,19 +61,19 @@ class Stories extends Component {
    {t('streams.sort_by')}
    - {t('streams.newest')} - {t('streams.oldest')} + {t('streams.newest')} + {t('streams.oldest')}
    { - assetsIds.length + rows.length ?
    - + {t('streams.article')} {t('streams.pubdate')} @@ -103,7 +97,7 @@ class Stories extends Component { Stories.propTypes = { assets: PropTypes.object, searchValue: PropTypes.string, - sort: PropTypes.string, + asc: PropTypes.string, filter: PropTypes.string, onStatusChange: PropTypes.func.isRequired, onSearchChange: PropTypes.func.isRequired, diff --git a/client/coral-admin/src/routes/Stories/containers/Stories.js b/client/coral-admin/src/routes/Stories/containers/Stories.js index 806bfc395..d91bed968 100644 --- a/client/coral-admin/src/routes/Stories/containers/Stories.js +++ b/client/coral-admin/src/routes/Stories/containers/Stories.js @@ -29,11 +29,11 @@ class StoriesContainer extends Component { } fetchAssets = (query) => { - const {searchValue, sort, filter, limit} = this.props; + const {searchValue, asc, filter, limit} = this.props; this.props.fetchAssets({ value: searchValue, - sort, + asc, filter, limit, ...query @@ -61,7 +61,7 @@ class StoriesContainer extends Component { return ({ assets: stories.assets, searchValue: stories.searchValue, - sort: stories.criteria.sort, + asc: stories.criteria.asc, filter: stories.criteria.filter, limit: stories.criteria.limit, }); @@ -92,7 +92,7 @@ const mapDispatchToProps = (dispatch) => StoriesContainer.propTypes = { assets: PropTypes.object, searchValue: PropTypes.string, - sort: PropTypes.string, + asc: PropTypes.string, filter: PropTypes.string, limit: PropTypes.number, setPage: PropTypes.func.isRequired, diff --git a/routes/api/assets/index.js b/routes/api/assets/index.js index 4007517de..165b2cd99 100644 --- a/routes/api/assets/index.js +++ b/routes/api/assets/index.js @@ -39,7 +39,7 @@ router.get('/', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, nex const { value = '', field = 'created_at', - page = 1, + page = 1, asc = 'false', filter = 'all', limit = 20, From dbd7f26311e325382b4f3e55decda4a2656dfb0f Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 25 Oct 2017 15:57:57 -0300 Subject: [PATCH 39/40] Adding pause after login --- test/e2e/specs/02_admin.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/e2e/specs/02_admin.js b/test/e2e/specs/02_admin.js index 463249eeb..03f460c1b 100644 --- a/test/e2e/specs/02_admin.js +++ b/test/e2e/specs/02_admin.js @@ -16,7 +16,11 @@ module.exports = { .setValue('@emailInput', admin.email) .setValue('@passwordInput', admin.password) .waitForElementVisible('@signInButton') - .click('@signInButton') + .click('@signInButton'); + + client.pause(3000); + + adminPage .waitForElementVisible('@moderationContainer'); }, From 6647e22d1cf894e8863f486da292f085920a107f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Wed, 25 Oct 2017 14:05:04 -0600 Subject: [PATCH 40/40] Update circle.yml --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 9f35736c6..932643ff6 100644 --- a/circle.yml +++ b/circle.yml @@ -50,7 +50,7 @@ test: - MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test # Check dependancies using nsp. - nsp check - - yarn e2e-ci + # - yarn e2e-ci deployment: release: