From cd1d2430bb8343b17bf1c7a0134f12150da6a54c Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 3 Dec 2017 00:00:23 -0800 Subject: [PATCH] =?UTF-8?q?Added=20a=20setting=20to=20update=20user?= =?UTF-8?q?=E2=80=99s=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scenes/Settings/Settings.js | 84 ++++++++++++++++++---- server/api/__snapshots__/user.test.js.snap | 23 ++++++ server/api/middlewares/validation.js | 6 ++ server/api/user.js | 11 +++ server/api/user.test.js | 28 ++++++++ 5 files changed, 139 insertions(+), 13 deletions(-) diff --git a/app/scenes/Settings/Settings.js b/app/scenes/Settings/Settings.js index 6172b1b4..8c23c85a 100644 --- a/app/scenes/Settings/Settings.js +++ b/app/scenes/Settings/Settings.js @@ -1,17 +1,65 @@ // @flow import React, { Component } from 'react'; +import { observable, runInAction } from 'mobx'; import { observer, inject } from 'mobx-react'; +import invariant from 'invariant'; +import styled from 'styled-components'; +import { color, size } from 'shared/styles/constants'; +import { client } from 'utils/ApiClient'; import AuthStore from 'stores/AuthStore'; +import ErrorsStore from 'stores/ErrorsStore'; import Input from 'components/Input'; +import Button from 'components/Button'; import CenteredContent from 'components/CenteredContent'; import PageTitle from 'components/PageTitle'; -import HelpText from 'components/HelpText'; @observer class Settings extends Component { + timeout: number; props: { auth: AuthStore, + errors: ErrorsStore, + }; + + @observable name: string; + @observable updated: boolean; + @observable isSaving: boolean; + + componentDidMount() { + if (this.props.auth.user) { + this.name = this.props.auth.user.name; + } + } + + componentWillUnmount() { + clearTimeout(this.timeout); + } + + handleSubmit = async (ev: SyntheticEvent) => { + ev.preventDefault(); + this.isSaving = true; + + try { + const res = await client.post(`/user.update`, { + name: this.name, + }); + invariant(res && res.data, 'Document list not available'); + const { data } = res; + runInAction('Settings#handleSubmit', () => { + this.props.auth.user = data; + this.updated = true; + this.timeout = setTimeout(() => (this.updated = false), 2500); + }); + } catch (e) { + this.props.errors.add('Failed to load documents'); + } finally { + this.isSaving = false; + } + }; + + handleNameChange = (ev: SyntheticInputEvent) => { + this.name = ev.target.value; }; render() { @@ -22,22 +70,32 @@ class Settings extends Component {

Profile

- - You’re signed in to Outline with Slack. To update your profile - information here please{' '} - - update your profile on Slack - {' '} - and re-login to refresh. - -
- - + + + + + Profile updated! +
); } } -export default inject('auth')(Settings); +const SuccessMessage = styled.span` + margin-left: ${size.large}; + color: ${color.slate}; + opacity: ${props => (props.visible ? 1 : 0)}; + + transition: opacity 0.25s; +`; + +export default inject('auth', 'errors', 'auth')(Settings); diff --git a/server/api/__snapshots__/user.test.js.snap b/server/api/__snapshots__/user.test.js.snap index 888d2362..6ad5d9af 100644 --- a/server/api/__snapshots__/user.test.js.snap +++ b/server/api/__snapshots__/user.test.js.snap @@ -22,3 +22,26 @@ Object { "status": 200, } `; + +exports[`#user.update should require authentication 1`] = ` +Object { + "error": "authentication_required", + "message": "Authentication required", + "ok": false, + "status": 401, +} +`; + +exports[`#user.update should update user profile information 1`] = ` +Object { + "data": Object { + "avatarUrl": "http://example.com/avatar.png", + "email": "user1@example.com", + "id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61", + "name": "New name", + "username": "user1", + }, + "ok": true, + "status": 200, +} +`; diff --git a/server/api/middlewares/validation.js b/server/api/middlewares/validation.js index d7ef52bc..231595cf 100644 --- a/server/api/middlewares/validation.js +++ b/server/api/middlewares/validation.js @@ -11,6 +11,12 @@ export default function validation() { } }; + ctx.assertNotEmpty = function assertNotEmpty(value, message) { + if (value === '') { + throw apiError(400, 'validation_error', message); + } + }; + ctx.assertEmail = (value, message) => { if (!validator.isEmail(value)) { throw apiError(400, 'validation_error', message); diff --git a/server/api/user.js b/server/api/user.js index b50b0bd1..ccc3a487 100644 --- a/server/api/user.js +++ b/server/api/user.js @@ -11,6 +11,17 @@ router.post('user.info', auth(), async ctx => { ctx.body = { data: await presentUser(ctx, ctx.state.user) }; }); +router.post('user.update', auth(), async ctx => { + const { user } = ctx.state; + const { name } = ctx.body; + ctx.assertNotEmpty(name, "name can't be empty"); + + if (name) user.name = name; + await user.save(); + + ctx.body = { data: await presentUser(ctx, user) }; +}); + router.post('user.s3Upload', auth(), async ctx => { const { filename, kind, size } = ctx.body; ctx.assertPresent(filename, 'filename is required'); diff --git a/server/api/user.test.js b/server/api/user.test.js index 847f3319..e3bc2644 100644 --- a/server/api/user.test.js +++ b/server/api/user.test.js @@ -38,3 +38,31 @@ describe('#user.info', async () => { expect(body).toMatchSnapshot(); }); }); + +describe('#user.update', async () => { + it('should update user profile information', async () => { + await seed(); + const user = await User.findOne({ + where: { + email: 'user1@example.com', + }, + }); + + const res = await server.post('/api/user.update', { + body: { token: user.getJwtToken(), name: 'New name' }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body).toMatchSnapshot(); + }); + + it('should require authentication', async () => { + await seed(); + const res = await server.post('/api/user.update'); + const body = await res.json(); + + expect(res.status).toEqual(401); + expect(body).toMatchSnapshot(); + }); +});