diff --git a/resources/scripts/api/admin/users/createUser.ts b/resources/scripts/api/admin/users/createUser.ts index d6ddc51da..fe8588017 100644 --- a/resources/scripts/api/admin/users/createUser.ts +++ b/resources/scripts/api/admin/users/createUser.ts @@ -1,11 +1,17 @@ import http from '@/api/http'; import { User, rawDataToUser } from '@/api/admin/users/getUsers'; +import { Values } from '@/api/admin/users/updateUser'; -export default (name: string, include: string[] = []): Promise => { +export type { Values }; + +export default (values: Values, include: string[] = []): Promise => { + 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.post('/api/application/users', { - name, - }, { params: { include: include.join(',') } }) + http.post('/api/application/users', data, { params: { include: include.join(',') } }) .then(({ data }) => resolve(rawDataToUser(data))) .catch(reject); }); diff --git a/resources/scripts/api/admin/users/getUsers.ts b/resources/scripts/api/admin/users/getUsers.ts index 8876c6b09..a081c6927 100644 --- a/resources/scripts/api/admin/users/getUsers.ts +++ b/resources/scripts/api/admin/users/getUsers.ts @@ -15,6 +15,7 @@ export interface User { rootAdmin: boolean; tfa: boolean; avatarURL: string; + roleId: number | null; roleName: string | null; createdAt: Date; updatedAt: Date; @@ -32,6 +33,7 @@ export const rawDataToUser = ({ attributes }: FractalResponseData): User => ({ rootAdmin: attributes.root_admin, tfa: attributes['2fa'], avatarURL: attributes.avatar_url, + roleId: attributes.role_id, roleName: attributes.role_name, createdAt: new Date(attributes.created_at), updatedAt: new Date(attributes.updated_at), diff --git a/resources/scripts/api/admin/users/updateUser.ts b/resources/scripts/api/admin/users/updateUser.ts new file mode 100644 index 000000000..e13f2143f --- /dev/null +++ b/resources/scripts/api/admin/users/updateUser.ts @@ -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, include: string[] = []): Promise => { + 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); + }); +}; diff --git a/resources/scripts/components/admin/users/NewUserContainer.tsx b/resources/scripts/components/admin/users/NewUserContainer.tsx index 19a64e9d6..7086efd16 100644 --- a/resources/scripts/components/admin/users/NewUserContainer.tsx +++ b/resources/scripts/components/admin/users/NewUserContainer.tsx @@ -1,8 +1,31 @@ import React from 'react'; import tw from 'twin.macro'; 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 () => { + const history = useHistory(); + + const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); + + const submit = (values: Values, { setSubmitting }: FormikHelpers) => { + 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 (
@@ -11,6 +34,10 @@ export default () => {

Add a new user to the panel.

+ + + +
); }; diff --git a/resources/scripts/components/admin/users/UserDeleteButton.tsx b/resources/scripts/components/admin/users/UserDeleteButton.tsx new file mode 100644 index 000000000..f51eca27a --- /dev/null +++ b/resources/scripts/components/admin/users/UserDeleteButton.tsx @@ -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) => 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 ( + <> + setVisible(false)} + > + Are you sure you want to delete this user? + + + + + ); +}; diff --git a/resources/scripts/components/admin/users/UserEditContainer.tsx b/resources/scripts/components/admin/users/UserEditContainer.tsx index b7cf17c46..d59e8b3ab 100644 --- a/resources/scripts/components/admin/users/UserEditContainer.tsx +++ b/resources/scripts/components/admin/users/UserEditContainer.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; 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 { User } from '@/api/admin/users/getUsers'; import getUser from '@/api/admin/users/getUser'; @@ -8,6 +8,14 @@ import AdminContentBlock from '@/components/admin/AdminContentBlock'; import Spinner from '@/components/elements/Spinner'; import FlashMessageRender from '@/components/FlashMessageRender'; 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 { user: User | undefined; @@ -22,7 +30,172 @@ export const Context = createContextStore({ }), }); -const UserEditContainer = () => { +export interface Params { + title: string; + initialValues?: Values; + children?: React.ReactNode; + + onSubmit: (values: Values, helpers: FormikHelpers) => void; + exists?: boolean; +} + +export function InformationContainer ({ title, initialValues, children, onSubmit, exists }: Params) { + const submit = (values: Values, helpers: FormikHelpers) => { + onSubmit(values, helpers); + }; + + if (!initialValues) { + initialValues = { + username: '', + email: '', + firstName: '', + lastName: '', + password: '', + roleId: 0, + }; + } + + return ( + + { + ({ isSubmitting, isValid }) => ( + <> + + + +
+
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+ {children} +
+ +
+
+ + + + ) + } + + ); +} + +function EditInformationContainer () { + const history = useHistory(); + + const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => 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) => { + clearFlashes('user'); + + updateUser(user.id, values) + .then(() => setUser({ ...user, ...values })) + .catch(error => { + console.error(error); + clearAndAddHttpError({ key: 'user', error }); + }) + .then(() => setSubmitting(false)); + }; + + return ( + +
+ history.push('/admin/users')} + /> +
+
+ ); +} + +function UserEditContainer () { const match = useRouteMatch<{ id?: string }>(); const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions) => actions.flashes); @@ -65,9 +238,11 @@ const UserEditContainer = () => {
+ + ); -}; +} export default () => { return (