From 657e11ff55a5f6de534a15c2d7d8b19c187f9cb1 Mon Sep 17 00:00:00 2001 From: Dwayne Harris Date: Thu, 10 Oct 2019 15:36:45 -0400 Subject: [PATCH] WIP --- src/actions/authentication.ts | 28 ++++++++++++++ src/actions/forms.ts | 4 +- src/actions/registration.ts | 6 ++- src/api/errors.ts | 9 +++++ src/components/app.tsx | 20 ++++++++-- src/components/create-user-form.tsx | 10 ++++- src/components/pages/create-app.tsx | 34 ++++++++++++++++- src/components/pages/developers.tsx | 3 +- src/components/pages/group-admin.tsx | 7 ++-- src/components/pages/login.tsx | 4 +- src/components/pages/register-group.tsx | 12 +++--- src/components/pages/register.tsx | 22 ++++++----- src/components/pages/self.tsx | 50 ++++++++++++++++++------- src/components/user-info.tsx | 2 - src/constants/index.ts | 7 ++++ src/reducers/forms.ts | 3 +- src/styles/app.scss | 2 +- src/types/entities.ts | 3 ++ src/types/store.ts | 1 + src/utils/normalization.ts | 3 +- 20 files changed, 183 insertions(+), 47 deletions(-) diff --git a/src/actions/authentication.ts b/src/actions/authentication.ts index ef7c558..04cec8f 100644 --- a/src/actions/authentication.ts +++ b/src/actions/authentication.ts @@ -106,3 +106,31 @@ export const authenticate = (name: string, password: string): AppThunkAction async dispatch => { + dispatch(startRequest(RequestKey.UpdateSelf)) + + try { + const self = await apiFetch({ + path: '/api/self', + method: 'put', + body: { + name, + about, + requiresApproval, + privacy, + }, + }) + + const result = normalize([self], EntityType.User) + + dispatch(setEntities(result.entities)) + dispatch(setUser(self.id)) + dispatch(setAuthenticated(true)) + + dispatch(finishRequest(RequestKey.UpdateSelf, true)) + } catch (err) { + dispatch(finishRequest(RequestKey.UpdateSelf, false)) + throw err + } +} diff --git a/src/actions/forms.ts b/src/actions/forms.ts index fcfbc05..a9515e4 100644 --- a/src/actions/forms.ts +++ b/src/actions/forms.ts @@ -9,6 +9,7 @@ export interface InitFieldAction extends Action { type: 'FORMS_INIT_FIELD' payload: { name: string + value: FormValue apiName?: string } } @@ -44,10 +45,11 @@ export const initForm = (): InitFormAction => ({ type: 'FORMS_INIT', }) -export const initField = (name: string, apiName?: string): InitFieldAction => ({ +export const initField = (name: string, value: FormValue, apiName?: string): InitFieldAction => ({ type: 'FORMS_INIT_FIELD', payload: { name, + value, apiName, }, }) diff --git a/src/actions/registration.ts b/src/actions/registration.ts index b8a4669..79a4a5f 100644 --- a/src/actions/registration.ts +++ b/src/actions/registration.ts @@ -118,6 +118,8 @@ interface RegisterOptions { email: string password: string name?: string + requiresApproval: boolean + privacy: string group?: string } @@ -128,7 +130,7 @@ interface RegisterResponse { } export const register = (options: RegisterOptions): AppThunkAction => async dispatch => { - const { id, email, password, name, group } = options + const { id, email, password, name, requiresApproval, privacy, group } = options dispatch(startRequest(RequestKey.Register)) @@ -141,6 +143,8 @@ export const register = (options: RegisterOptions): AppThunkAction => as email, password, name, + requiresApproval, + privacy, group, }, }) diff --git a/src/api/errors.ts b/src/api/errors.ts index df2ce54..fe38aea 100644 --- a/src/api/errors.ts +++ b/src/api/errors.ts @@ -5,8 +5,11 @@ import { showNotification } from 'src/actions/notifications' import { AppThunkDispatch, FormNotification, NotificationType } from 'src/types' export function handleApiError(err: HttpError, dispatch: AppThunkDispatch, history?: History) { + console.error('Error:', err) + if (err instanceof ServerError) { dispatch(showNotification(NotificationType.Error, 'Server Error')) + return } if (err instanceof BadRequestError) { @@ -16,16 +19,22 @@ export function handleApiError(err: HttpError, dispatch: AppThunkDispatch, histo const { field, type, message } = error if (field) dispatch(setFieldNotification(field, type, message)) } + + return } if (err instanceof UnauthorizedError) { dispatch(showNotification(NotificationType.Error, 'You need to be logged in.')) if (history) history.push('/login') + return } if (err instanceof NotFoundError) { dispatch(showNotification(NotificationType.Error, 'Not found.')) + return } + + dispatch(showNotification(NotificationType.Error, `Error: ${err.message}`)) } export class HttpError extends Error { diff --git a/src/components/app.tsx b/src/components/app.tsx index 817c870..42d6e17 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -1,7 +1,8 @@ import React, { FC, useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' -import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom' +import { BrowserRouter as Router, Route, Switch, Link, useHistory } from 'react-router-dom' +import { handleApiError } from 'src/api/errors' import { fetchSelf, setChecked } from 'src/actions/authentication' import { getFetching } from 'src/selectors' import { getCollapsed } from 'src/selectors/menu' @@ -16,6 +17,7 @@ import Spinner from './spinner' import UserInfo from './user-info' import About from './pages/about' +import CreateApp from './pages/create-app' import Developers from './pages/developers' import Group from './pages/group' import GroupAdmin from './pages/group-admin' @@ -38,12 +40,21 @@ const App: FC = () => { const mainMenuWidth = 275 const mainColumnMargin = collapsed ? 0 : mainMenuWidth - useEffect(() => { + const init = async () => { if (localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY)) { - dispatch(fetchSelf()) + try { + await dispatch(fetchSelf()) + } catch (err) { + console.log('err', err) + handleApiError(err, dispatch) + } } else { dispatch(setChecked()) } + } + + useEffect(() => { + init() }, []) return ( @@ -85,6 +96,9 @@ const App: FC = () => { + + + diff --git a/src/components/create-user-form.tsx b/src/components/create-user-form.tsx index a89fbbb..962b57f 100644 --- a/src/components/create-user-form.tsx +++ b/src/components/create-user-form.tsx @@ -1,12 +1,14 @@ import React, { FC } from 'react' import { useDispatch } from 'react-redux' import { Link } from 'react-router-dom' -import { faEnvelope, faIdCard } from '@fortawesome/free-solid-svg-icons' +import { faEnvelope, faIdCard, faUserShield } from '@fortawesome/free-solid-svg-icons' import { checkUserAvailability } from 'src/actions/registration' +import { PRIVACY_OPTIONS } from 'src/constants' import CheckboxField from './forms/checkbox-field' import TextField from './forms/text-field' import PasswordField from './forms/password-field' +import SelectField from './forms/select-field' const CreateUserForm: FC = () => { const dispatch = useDispatch() @@ -27,6 +29,12 @@ const CreateUserForm: FC = () => {

+ +
+ + You must approve each Subscription request from other users. + +
I agree to the User terms and conditions. diff --git a/src/components/pages/create-app.tsx b/src/components/pages/create-app.tsx index ae6f076..77463b8 100644 --- a/src/components/pages/create-app.tsx +++ b/src/components/pages/create-app.tsx @@ -1,12 +1,28 @@ import React, { FC, useEffect } from 'react' +import { useDispatch } from 'react-redux' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCheckCircle } from '@fortawesome/free-solid-svg-icons' + +import { initForm, initField } from 'src/actions/forms' import { setTitle } from 'src/utils' import PageHeader from 'src/components/page-header' +import TextField from 'src/components/forms/text-field' +import TextareaField from 'src/components/forms/textarea-field' const CreateApp: FC = () => { + const dispatch = useDispatch() + useEffect(() => { setTitle('Create a new App') - }) + + dispatch(initForm()) + dispatch(initField('name', '')) + dispatch(initField('about', '')) + dispatch(initField('websiteUrl', '')) + dispatch(initField('companyName', '')) + dispatch(initField('version', '')) + }, []) return (
@@ -14,7 +30,23 @@ const CreateApp: FC = () => {
+ +
+ +
+ +
+ +
+ +
+
diff --git a/src/components/pages/developers.tsx b/src/components/pages/developers.tsx index 9913642..4312eaf 100644 --- a/src/components/pages/developers.tsx +++ b/src/components/pages/developers.tsx @@ -27,8 +27,9 @@ const Developers: FC = () => {

This is where you manage apps you create.

+
- + diff --git a/src/components/pages/group-admin.tsx b/src/components/pages/group-admin.tsx index 5109022..7d23216 100644 --- a/src/components/pages/group-admin.tsx +++ b/src/components/pages/group-admin.tsx @@ -63,10 +63,9 @@ const GroupAdmin: FC = () => { } dispatch(initForm()) - dispatch(initField('about')) - dispatch(initField('expiration')) - dispatch(initField('limit')) - dispatch(setFieldValue('about', group.about as string)) + dispatch(initField('about', group.about)) + dispatch(initField('expiration', '0')) + dispatch(initField('limit', '0')) setTitle(`${group.name} Administration`) } diff --git a/src/components/pages/login.tsx b/src/components/pages/login.tsx index db0ea8a..775a56f 100644 --- a/src/components/pages/login.tsx +++ b/src/components/pages/login.tsx @@ -35,8 +35,8 @@ const Login: FC = () => { useEffect(() => { dispatch(initForm()) - dispatch(initField('name', 'id')) - dispatch(initField('password')) + dispatch(initField('name', '', 'id')) + dispatch(initField('password', '')) }, []) const handleAuthenticate = async () => { diff --git a/src/components/pages/register-group.tsx b/src/components/pages/register-group.tsx index 2341aff..23ff778 100644 --- a/src/components/pages/register-group.tsx +++ b/src/components/pages/register-group.tsx @@ -39,11 +39,11 @@ const RegisterGroup: FC = () => { handleApiError(err, dispatch, history) } - dispatch(initField('user-id', 'id')) - dispatch(initField('user-name', 'name')) - dispatch(initField('user-email', 'email')) - dispatch(initField('password')) - dispatch(initField('user-agree')) + dispatch(initField('user-id', '', 'id')) + dispatch(initField('user-name', '', 'name')) + dispatch(initField('user-email', '', 'email')) + dispatch(initField('password', '')) + dispatch(initField('user-agree', false)) setTitle('Register') }, []) @@ -64,6 +64,8 @@ const RegisterGroup: FC = () => { email: valueFromForm(form, 'user-email', ''), password: valueFromForm(form, 'password', ''), name: valueFromForm(form, 'user-name', ''), + requiresApproval: valueFromForm(form, 'user-requires-approval', false), + privacy: valueFromForm(form, 'user-privacy', 'public'), group: id, })) diff --git a/src/components/pages/register.tsx b/src/components/pages/register.tsx index 08b334f..3d32982 100644 --- a/src/components/pages/register.tsx +++ b/src/components/pages/register.tsx @@ -38,6 +38,8 @@ const Register: FC = () => { email: valueFromForm(form, 'user-email', ''), password: valueFromForm(form, 'password', ''), name: valueFromForm(form, 'user-name', ''), + requiresApproval: valueFromForm(form, 'user-requires-approval', true), + privacy: valueFromForm(form, 'user-privacy', 'open') })) await dispatch(createGroup({ @@ -69,16 +71,16 @@ const Register: FC = () => { useEffect(() => { dispatch(initForm()) - dispatch(initField('group-name', 'name')) - dispatch(initField('group-registration', 'registration')) - dispatch(initField('group-agree')) - dispatch(initField('user-id', 'id')) - dispatch(initField('user-name', 'name')) - dispatch(initField('user-email', 'email')) - dispatch(initField('password')) - dispatch(initField('user-agree')) - - dispatch(setFieldValue('group-registration', 'open')) + dispatch(initField('group-name', '', 'name')) + dispatch(initField('group-registration', 'open', 'registration')) + dispatch(initField('group-agree', false)) + dispatch(initField('user-id', '', 'id')) + dispatch(initField('user-name', '', 'name')) + dispatch(initField('user-email', '', 'email')) + dispatch(initField('password', '')) + dispatch(initField('user-requires-approval', true, 'requiresApproval')) + dispatch(initField('user-privacy', 'public', 'privacy')) + dispatch(initField('user-agree', false)) }, []) useEffect(() => { diff --git a/src/components/pages/self.tsx b/src/components/pages/self.tsx index 9175601..de5f048 100644 --- a/src/components/pages/self.tsx +++ b/src/components/pages/self.tsx @@ -3,20 +3,25 @@ import { useSelector, useDispatch } from 'react-redux' import { Link, useParams, useHistory } from 'react-router-dom' import moment from 'moment' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faDoorOpen, faCheckCircle, faPlusCircle, faIdCard, faEnvelope } from '@fortawesome/free-solid-svg-icons' +import { faDoorOpen, faCheckCircle, faPlusCircle, faIdCard, faEnvelope, faUserShield } from '@fortawesome/free-solid-svg-icons' -import { unauthenticate } from 'src/actions/authentication' -import { initForm, initField, setFieldValue } from 'src/actions/forms' +import { unauthenticate, updateSelf } from 'src/actions/authentication' +import { initForm, initField } from 'src/actions/forms' import { getAuthenticated, getChecked, getAuthenticatedUser } from 'src/selectors/authentication' +import { getForm } from 'src/selectors/forms' +import { handleApiError } from 'src/api/errors' +import { PRIVACY_OPTIONS } from 'src/constants' import { useAuthenticationCheck, useDeepCompareEffect } from 'src/hooks' -import { setTitle } from 'src/utils' -import { AppState, User, Tab } from 'src/types' +import { setTitle, valueFromForm } from 'src/utils' +import { AppState, User, Tab, Form } from 'src/types' import PageHeader from 'src/components/page-header' import Loading from 'src/components/pages/loading' import TextField from 'src/components/forms/text-field' import TextareaField from 'src/components/forms/textarea-field' +import SelectField from 'src/components/forms/select-field' +import CheckboxField from 'src/components/forms/checkbox-field' interface Params { tab: string @@ -36,6 +41,7 @@ const Self: FC = () => { const checked = useSelector(getChecked) const authenticated = useSelector(getAuthenticated) const user = useSelector(getAuthenticatedUser) + const form = useSelector(getForm) useAuthenticationCheck(checked, authenticated, history) @@ -45,15 +51,28 @@ const Self: FC = () => { window.location.href = '/' } + const handleUpdate = () => { + const name = valueFromForm(form, 'name', '') + const about = valueFromForm(form, 'about', '') + const requiresApproval = valueFromForm(form, 'requiresApproval', true) + const privacy = valueFromForm(form, 'privacy', 'public') + + try { + dispatch(updateSelf(name, about, requiresApproval, privacy)) + } catch (err) { + handleApiError(err, dispatch, history) + } + } + useDeepCompareEffect(() => { if (user) { setTitle(`${user.name} (@${user.id})`) dispatch(initForm()) - dispatch(initField('name')) - dispatch(initField('about')) - dispatch(setFieldValue('name', user.name as string)) - dispatch(setFieldValue('about', user.about as string)) + dispatch(initField('name', user.name)) + dispatch(initField('about', user.about || '')) + dispatch(initField('requiresApproval', user.requiresApproval)) + dispatch(initField('privacy', user.privacy)) } }, [user]) @@ -117,11 +136,16 @@ const Self: FC = () => {
-
+ +
+ + You must approve each Subscription request from other users. + +

- +
}
diff --git a/src/components/user-info.tsx b/src/components/user-info.tsx index 3cd4e73..b337dc7 100644 --- a/src/components/user-info.tsx +++ b/src/components/user-info.tsx @@ -5,10 +5,8 @@ import { Link } from 'react-router-dom' import { getAuthenticatedUser } from 'src/selectors/authentication' import { AppState, User } from 'src/types' - const UserInfo: FC = () => { const user = useSelector(getAuthenticatedUser) - const hasAvatar = user && user.imageUrl const imageUrl = hasAvatar ? user!.imageUrl : undefined diff --git a/src/constants/index.ts b/src/constants/index.ts index dd200f4..16a0334 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -4,3 +4,10 @@ export const MAX_NAME_LENGTH = 80 export const LOCAL_STORAGE_ACCESS_TOKEN_KEY = 'FLEXOR_ACCESS_TOKEN' export const LOCAL_STORAGE_ACCESS_TOKEN_AT_KEY = 'FLEXOR_ACCESS_TOKEN_AT' export const LOCAL_STORAGE_REFRESH_TOKEN_KEY = 'FLEXOR_REFRESH_TOKEN' + +export const PRIVACY_OPTIONS = { + public: 'Anyone can see your posts', + group: 'Only the people in your community can see your posts', + subscribers: 'Only your subscribers can see your posts', + private: 'Nobody can see your posts', +} diff --git a/src/reducers/forms.ts b/src/reducers/forms.ts index 3678432..d93d9c4 100644 --- a/src/reducers/forms.ts +++ b/src/reducers/forms.ts @@ -17,7 +17,7 @@ const reducer: Reducer = (state = initialState, action notification: undefined, } case 'FORMS_INIT_FIELD': - const { name, apiName } = action.payload + const { name, value, apiName } = action.payload return { ...state, @@ -25,6 +25,7 @@ const reducer: Reducer = (state = initialState, action ...state.form, [name]: { name, + value, apiName, }, }, diff --git a/src/styles/app.scss b/src/styles/app.scss index d2f7e28..3866ddf 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -40,7 +40,7 @@ div#main-menu { bottom: 0; display: flex; flex-direction: column; - position: absolute; + position: fixed; right: 0; top: 0; } diff --git a/src/types/entities.ts b/src/types/entities.ts index fa2633b..4253f78 100644 --- a/src/types/entities.ts +++ b/src/types/entities.ts @@ -21,6 +21,7 @@ export interface Entity { export type Group = Entity & { name: string membership?: GroupMembershipType + about: string } export type Installation = { @@ -35,6 +36,8 @@ export type User = Entity & { about?: string imageUrl?: string coverImageUrl?: string + requiresApproval: boolean + privacy: string installations: Installation[] } diff --git a/src/types/store.ts b/src/types/store.ts index fcd4584..69473c8 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -23,6 +23,7 @@ export enum RequestKey { FetchInvitations = 'fetch_invitations', FetchApps = 'fetch_apps', FetchSelfApps = 'fetch_self_apps', + UpdateSelf = 'update_self', } export type FormValue = string | number | boolean diff --git a/src/utils/normalization.ts b/src/utils/normalization.ts index 6037941..186a1d5 100644 --- a/src/utils/normalization.ts +++ b/src/utils/normalization.ts @@ -53,11 +53,12 @@ export function normalize(entities: Entity[], type: EntityType): NormalizeResult case EntityType.User: keys = entities.map(entity => { const user = entity as User + const { installations = [] } = user return set(type, newStore, { ...user, group: set(EntityType.Group, newStore, user.group), - installations: user.installations.map(installation => { + installations: installations.map(installation => { return { ...installation, app: set(EntityType.App, newStore, installation.app),