ui(admin): add user create and user update

This commit is contained in:
Matthew Penner 2021-07-24 12:23:33 -06:00
parent 1d290919b7
commit 27c93365e9
6 changed files with 299 additions and 7 deletions

View file

@ -1,11 +1,17 @@
import http from '@/api/http'; import http from '@/api/http';
import { User, rawDataToUser } from '@/api/admin/users/getUsers'; import { User, rawDataToUser } from '@/api/admin/users/getUsers';
import { Values } from '@/api/admin/users/updateUser';
export default (name: string, include: string[] = []): Promise<User> => { export type { Values };
export default (values: Values, include: string[] = []): Promise<User> => {
const data = {};
Object.keys(values).forEach(k => {
// @ts-ignore
data[k.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`)] = values[k];
});
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.post('/api/application/users', { http.post('/api/application/users', data, { params: { include: include.join(',') } })
name,
}, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToUser(data))) .then(({ data }) => resolve(rawDataToUser(data)))
.catch(reject); .catch(reject);
}); });

View file

@ -15,6 +15,7 @@ export interface User {
rootAdmin: boolean; rootAdmin: boolean;
tfa: boolean; tfa: boolean;
avatarURL: string; avatarURL: string;
roleId: number | null;
roleName: string | null; roleName: string | null;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
@ -32,6 +33,7 @@ export const rawDataToUser = ({ attributes }: FractalResponseData): User => ({
rootAdmin: attributes.root_admin, rootAdmin: attributes.root_admin,
tfa: attributes['2fa'], tfa: attributes['2fa'],
avatarURL: attributes.avatar_url, avatarURL: attributes.avatar_url,
roleId: attributes.role_id,
roleName: attributes.role_name, roleName: attributes.role_name,
createdAt: new Date(attributes.created_at), createdAt: new Date(attributes.created_at),
updatedAt: new Date(attributes.updated_at), updatedAt: new Date(attributes.updated_at),

View file

@ -0,0 +1,24 @@
import http from '@/api/http';
import { User, rawDataToUser } from '@/api/admin/users/getUsers';
export interface Values {
username: string;
email: string;
firstName: string;
lastName: string;
password: string;
roleId: number | null;
}
export default (id: number, values: Partial<Values>, include: string[] = []): Promise<User> => {
const data = {};
Object.keys(values).forEach(k => {
// @ts-ignore
data[k.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`)] = values[k];
});
return new Promise((resolve, reject) => {
http.patch(`/api/application/users/${id}`, data, { params: { include: include.join(',') } })
.then(({ data }) => resolve(rawDataToUser(data)))
.catch(reject);
});
};

View file

@ -1,8 +1,31 @@
import React from 'react'; import React from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import AdminContentBlock from '@/components/admin/AdminContentBlock'; import AdminContentBlock from '@/components/admin/AdminContentBlock';
import FlashMessageRender from '@/components/FlashMessageRender';
import { InformationContainer } from '@/components/admin/users/UserEditContainer';
import { useHistory } from 'react-router-dom';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import { FormikHelpers } from 'formik';
import createUser, { Values } from '@/api/admin/users/createUser';
export default () => { export default () => {
const history = useHistory();
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('user:create');
createUser(values)
.then(user => history.push(`/admin/users/${user.id}`))
.catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'user:create', error });
})
.then(() => setSubmitting(false));
};
return ( return (
<AdminContentBlock title={'New User'}> <AdminContentBlock title={'New User'}>
<div css={tw`w-full flex flex-row items-center mb-8`}> <div css={tw`w-full flex flex-row items-center mb-8`}>
@ -11,6 +34,10 @@ export default () => {
<p css={tw`text-base text-neutral-400 whitespace-nowrap overflow-ellipsis overflow-hidden`}>Add a new user to the panel.</p> <p css={tw`text-base text-neutral-400 whitespace-nowrap overflow-ellipsis overflow-hidden`}>Add a new user to the panel.</p>
</div> </div>
</div> </div>
<FlashMessageRender byKey={'user:create'} css={tw`mb-4`}/>
<InformationContainer title={'Create User'} onSubmit={submit}/>
</AdminContentBlock> </AdminContentBlock>
); );
}; };

View file

@ -0,0 +1,58 @@
import React, { useState } from 'react';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
import ConfirmationModal from '@/components/elements/ConfirmationModal';
import deleteUser from '@/api/admin/users/deleteUser';
interface Props {
userId: number;
onDeleted: () => void;
}
export default ({ userId, onDeleted }: Props) => {
const [ visible, setVisible ] = useState(false);
const [ loading, setLoading ] = useState(false);
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const onDelete = () => {
setLoading(true);
clearFlashes('user');
deleteUser(userId)
.then(() => {
setLoading(false);
onDeleted();
})
.catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'user', error });
setLoading(false);
setVisible(false);
});
};
return (
<>
<ConfirmationModal
visible={visible}
title={'Delete user?'}
buttonText={'Yes, delete user'}
onConfirmed={onDelete}
showSpinnerOverlay={loading}
onModalDismissed={() => setVisible(false)}
>
Are you sure you want to delete this user?
</ConfirmationModal>
<Button type={'button'} size={'xsmall'} color={'red'} onClick={() => setVisible(true)}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" css={tw`h-5 w-5`}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</Button>
</>
);
};

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import { useRouteMatch } from 'react-router-dom'; import { useHistory, useRouteMatch } from 'react-router-dom';
import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy'; import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy';
import { User } from '@/api/admin/users/getUsers'; import { User } from '@/api/admin/users/getUsers';
import getUser from '@/api/admin/users/getUser'; import getUser from '@/api/admin/users/getUser';
@ -8,6 +8,14 @@ import AdminContentBlock from '@/components/admin/AdminContentBlock';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import AdminBox from '@/components/admin/AdminBox';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { Form, Formik, FormikHelpers } from 'formik';
import { object, string } from 'yup';
import Field from '@/components/elements/Field';
import Button from '@/components/elements/Button';
import updateUser, { Values } from '@/api/admin/users/updateUser';
import UserDeleteButton from '@/components/admin/users/UserDeleteButton';
interface ctx { interface ctx {
user: User | undefined; user: User | undefined;
@ -22,7 +30,172 @@ export const Context = createContextStore<ctx>({
}), }),
}); });
const UserEditContainer = () => { export interface Params {
title: string;
initialValues?: Values;
children?: React.ReactNode;
onSubmit: (values: Values, helpers: FormikHelpers<Values>) => void;
exists?: boolean;
}
export function InformationContainer ({ title, initialValues, children, onSubmit, exists }: Params) {
const submit = (values: Values, helpers: FormikHelpers<Values>) => {
onSubmit(values, helpers);
};
if (!initialValues) {
initialValues = {
username: '',
email: '',
firstName: '',
lastName: '',
password: '',
roleId: 0,
};
}
return (
<Formik
onSubmit={submit}
initialValues={initialValues}
validationSchema={object().shape({
username: string().min(1).max(32),
email: string(),
firstName: string(),
lastName: string(),
password: exists ? string() : string().required(),
})}
>
{
({ isSubmitting, isValid }) => (
<>
<AdminBox title={title} css={tw`relative`}>
<SpinnerOverlay visible={isSubmitting}/>
<Form css={tw`mb-0`}>
<div css={tw`md:w-full md:flex md:flex-row`}>
<div css={tw`md:w-full md:flex md:flex-col md:mr-4 mt-6 md:mt-0`}>
<Field
id={'username'}
name={'username'}
label={'Username'}
type={'text'}
/>
</div>
<div css={tw`md:w-full md:flex md:flex-col md:ml-4 mt-6 md:mt-0`}>
<Field
id={'email'}
name={'email'}
label={'Email Address'}
type={'email'}
/>
</div>
</div>
<div css={tw`md:w-full md:flex md:flex-row mt-6`}>
<div css={tw`md:w-full md:flex md:flex-col md:mr-4 mt-6 md:mt-0`}>
<Field
id={'firstName'}
name={'firstName'}
label={'First Name'}
type={'text'}
/>
</div>
<div css={tw`md:w-full md:flex md:flex-col md:ml-4 mt-6 md:mt-0`}>
<Field
id={'lastName'}
name={'lastName'}
label={'Last Name'}
type={'text'}
/>
</div>
</div>
<div css={tw`md:w-full md:flex md:flex-row mt-6`}>
<div css={tw`md:w-full md:flex md:flex-col md:mr-4 mt-6 md:mt-0`}>
<Field
id={'password'}
name={'password'}
label={'Password'}
type={'password'}
placeholder={'••••••••'}
autoComplete={'new-password'}
/>
</div>
<div css={tw`md:w-full md:flex md:flex-col md:ml-4 mt-6 md:mt-0`}/>
</div>
<div css={tw`w-full flex flex-row items-center mt-6`}>
{children}
<div css={tw`flex ml-auto`}>
<Button type={'submit'} disabled={isSubmitting || !isValid}>
Save
</Button>
</div>
</div>
</Form>
</AdminBox>
</>
)
}
</Formik>
);
}
function EditInformationContainer () {
const history = useHistory();
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const user = Context.useStoreState(state => state.user);
const setUser = Context.useStoreActions(actions => actions.setUser);
if (user === undefined) {
return (
<></>
);
}
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('user');
updateUser(user.id, values)
.then(() => setUser({ ...user, ...values }))
.catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'user', error });
})
.then(() => setSubmitting(false));
};
return (
<InformationContainer
title={'Edit User'}
initialValues={{
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
roleId: user.roleId,
password: '',
}}
onSubmit={submit}
>
<div css={tw`flex`}>
<UserDeleteButton
userId={user.id}
onDeleted={() => history.push('/admin/users')}
/>
</div>
</InformationContainer>
);
}
function UserEditContainer () {
const match = useRouteMatch<{ id?: string }>(); const match = useRouteMatch<{ id?: string }>();
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
@ -65,9 +238,11 @@ const UserEditContainer = () => {
</div> </div>
<FlashMessageRender byKey={'user'} css={tw`mb-4`}/> <FlashMessageRender byKey={'user'} css={tw`mb-4`}/>
<EditInformationContainer/>
</AdminContentBlock> </AdminContentBlock>
); );
}; }
export default () => { export default () => {
return ( return (