From fa18ebbaaec9407e81a6f44853b35b940e828bdc Mon Sep 17 00:00:00 2001 From: Jovert Lota Palonpon Date: Sat, 30 Mar 2019 19:58:12 +0800 Subject: [PATCH] Redesigned the create form #25 --- app/Http/Controllers/Api/UsersController.php | 13 +- .../js/ui/Loaders/LinearIndeterminate.js | 23 + resources/js/ui/Loaders/index.js | 1 + .../js/views/__backoffice/users/Create.js | 608 +++++------------- .../views/__backoffice/users/Forms/Account.js | 202 ++++++ .../views/__backoffice/users/Forms/Avatar.js | 52 ++ .../views/__backoffice/users/Forms/Profile.js | 308 +++++++++ .../views/__backoffice/users/Forms/index.js | 5 + 8 files changed, 758 insertions(+), 454 deletions(-) create mode 100644 resources/js/ui/Loaders/LinearIndeterminate.js create mode 100644 resources/js/views/__backoffice/users/Forms/Account.js create mode 100644 resources/js/views/__backoffice/users/Forms/Avatar.js create mode 100644 resources/js/views/__backoffice/users/Forms/Profile.js create mode 100644 resources/js/views/__backoffice/users/Forms/index.js diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 8c850c2..0ba2ea1 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -32,19 +32,24 @@ public function index(Request $request) : JsonResponse public function store(Request $request) : JsonResponse { $request->validate([ - 'type' => 'required|in:superuser,user', - 'firstname' => 'required|string|max:255', - 'lastname' => 'required|string|max:255', + 'firstname' => 'required_if:step,0|string|max:255', + 'lastname' => 'required_if:step,0|string|max:255', 'gender' => 'nullable|in:female,male', 'birthdate' => 'nullable|date:Y-m-d|before:'.now()->subYear(10)->format('Y-m-d'), 'address' => 'nullable|string|max:510', - 'email' => 'required|email|unique:users,email,NULL,id,deleted_at,NULL', + 'type' => 'required_if:step,1|in:superuser,user', + 'email' => 'required_if:step,1|email|unique:users,email,NULL,id,deleted_at,NULL', 'username' => 'nullable|unique:users' ]); + // Return here if the user is just in the first step. + if ($request->input('step') === 0) { + return response()->json(200); + } + $user = User::create([ 'type' => $request->input('type'), 'firstname' => ($firstname = $request->input('firstname')), diff --git a/resources/js/ui/Loaders/LinearIndeterminate.js b/resources/js/ui/Loaders/LinearIndeterminate.js new file mode 100644 index 0000000..8a56753 --- /dev/null +++ b/resources/js/ui/Loaders/LinearIndeterminate.js @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { LinearProgress, withStyles } from '@material-ui/core'; + +const LinearIndeterminate = props => ( + +); + +const styles = theme => ({ + root: { + margin: `0 ${theme.spacing.unit}px`, + minHeight: theme.spacing.unit, + borderTopRightRadius: '100%', + borderTopLeftRadius: '100%', + }, +}); + +LinearIndeterminate.propTypes = { + classes: PropTypes.object.isRequired, +}; + +export default withStyles(styles)(LinearIndeterminate); diff --git a/resources/js/ui/Loaders/index.js b/resources/js/ui/Loaders/index.js index 78f06e0..58c7e19 100644 --- a/resources/js/ui/Loaders/index.js +++ b/resources/js/ui/Loaders/index.js @@ -1 +1,2 @@ export { default as LinearDeterminate } from './LinearDeterminate'; +export { default as LinearIndeterminate } from './LinearIndeterminate'; diff --git a/resources/js/views/__backoffice/users/Create.js b/resources/js/views/__backoffice/users/Create.js index 9e9f8fb..07a5ca1 100755 --- a/resources/js/views/__backoffice/users/Create.js +++ b/resources/js/views/__backoffice/users/Create.js @@ -1,33 +1,41 @@ import React, { Component } from 'react'; -import { Formik, Form } from 'formik'; -import * as Yup from 'yup'; import { - Button, - FormControl, - FormHelperText, - Input, - Grid, - InputLabel, - MenuItem, Paper, - Select, + Step, + StepLabel, + Stepper, Typography, withStyles, } from '@material-ui/core'; -import { MuiPickersUtilsProvider, DatePicker } from 'material-ui-pickers'; -import MomentUtils from '@date-io/moment'; - import * as NavigationUtils from '../../../utils/Navigation'; -import * as UrlUtils from '../../../utils/URL'; import { User } from '../../../models'; +import { LinearIndeterminate } from '../../../ui/Loaders'; import { Master as MasterLayout } from '../layouts'; +import { Profile, Account, Avatar } from './Forms'; + class Create extends Component { state = { loading: false, + activeStep: 0, + formValues: [], errors: {}, + message: {}, + }; + + /** + * This should return back to the previous step. + * + * @return {undefined} + */ + handleBack = () => { + this.setState(prevState => { + return { + activeStep: prevState.activeStep - 1, + }; + }); }; /** @@ -43,24 +51,53 @@ class Create extends Component { handleSubmit = async (values, { setSubmitting, setErrors }) => { setSubmitting(false); + // Stop here as it is the last step... + if (this.state.activeStep === 2) { + return; + } + this.setState({ loading: true }); try { - const { history } = this.props; + const { activeStep, formValues } = this.state; + let previousValues = {}; + + // Merge the form values here. + if (activeStep === 1) { + previousValues = formValues.reduce((prev, next) => { + return { ...prev, ...next }; + }); + } - await User.store(values); + // Instruct the API the current step. + values.step = activeStep; - this.setState({ loading: false }); + await User.store({ ...previousValues, ...values }); - const url = NavigationUtils._route('backoffice.users.index'); - const queryString = UrlUtils._queryString({ - '_message[type]': 'success', - '_message[body]': Lang.get('resources.created', { - name: 'User', - }), - }); + // After persisting the previous values. Move to the next step... + this.setState(prevState => { + let formValues = [...prevState.formValues]; + formValues[prevState.activeStep] = values; - history.push(`${url}${queryString}`); + let message = {}; + + if (prevState.activeStep === 1) { + message = { + type: 'success', + body: Lang.get('resources.created', { + name: 'User', + }), + closed: () => this.setState({ message: {} }), + }; + } + + return { + loading: false, + message, + formValues, + activeStep: prevState.activeStep + 1, + }; + }); } catch (error) { if (!error.response) { throw new Error('Unknown error'); @@ -69,435 +106,114 @@ class Create extends Component { const { errors } = error.response.data; setErrors(errors); + + this.setState({ loading: false }); } }; render() { const { classes, ...other } = this.props; - const { errors: formErrors } = this.state; - - const renderForm = ( - { - let mappedValues = {}; - let valuesArray = Object.values(values); - - // Format values specially the object ones (i.e Moment) - Object.keys(values).forEach((filter, key) => { - if ( - valuesArray[key] !== null && - typeof valuesArray[key] === 'object' && - valuesArray[key].hasOwnProperty('_isAMomentObject') - ) { - mappedValues[filter] = moment( - valuesArray[key], - ).format('YYYY-MM-DD'); - - return; - } - - mappedValues[filter] = valuesArray[key]; - }); - - await this.handleSubmit(mappedValues, form); - }} - validateOnBlur={false} - > - {({ - values, - handleChange, - setFieldValue, - errors, - isSubmitting, - }) => { - if (formErrors && Object.keys(formErrors).length > 0) { - errors = formErrors; - } + const { loading, activeStep, formValues, errors, message } = this.state; + + const steps = ['Profile', 'Account', 'Avatar']; + + const renderForm = () => { + switch (activeStep) { + case 0: + const defaultValues = { + firstname: '', + middlename: '', + lastname: '', + gender: '', + birthdate: null, + address: '', + }; return ( -
- - Personal Information - + + ); + break; - - - - - Firstname * - - - - - {errors.hasOwnProperty('firstname') && ( - - {errors.firstname} - - )} - - - - - - - Middlename - - - - - {errors.hasOwnProperty( - 'middlename', - ) && ( - - {errors.middlename} - - )} - - - - - - - Lastname * - - - - - {errors.hasOwnProperty('lastname') && ( - - {errors.lastname} - - )} - - - - - - - - - Gender - - - - - {errors.hasOwnProperty('gender') && ( - - {errors.gender} - - )} - - - - - - - - setFieldValue( - 'birthdate', - date, - ) - } - format="YYYY-MM-DD" - maxDate={moment() - .subtract(10, 'y') - .subtract(1, 'd') - .format('YYYY-MM-DD')} - keyboard - clearable - disableFuture - /> - - - {errors.hasOwnProperty('birthdate') && ( - - {errors.birthdate} - - )} - - - - - - - - - Address - - - - - {errors.hasOwnProperty('address') && ( - - {errors.address} - - )} - - - - -
- - - Account Settings - + case 1: + return ( + + ); + break; - - - - - Type * - - - - - {errors.hasOwnProperty('type') && ( - - {errors.type} - - )} - - - - - - - - - Email * - - - - - {errors.hasOwnProperty('email') && ( - - {errors.email} - - )} - - - - - - - Username - - - - - {errors.hasOwnProperty('username') && ( - - {errors.username} - - )} - - - - -
- - - - - - - + case 2: + return ( + + this.props.history.push( + NavigationUtils._route( + 'backoffice.users.index', + ), + ) + } + /> ); - }} - - ); + break; + + default: + throw new Error('Unknown step!'); + break; + } + }; return ( - - -
- - User Creation - - -
- - {renderForm} -
- + +
+ {loading && } + + +
+ + User Creation + + + + {steps.map(name => ( + + {name} + + ))} + + + {renderForm()} +
+
+
); } @@ -514,14 +230,6 @@ const styles = theme => ({ pageContent: { padding: theme.spacing.unit * 3, }, - - sectionSpacer: { - marginTop: theme.spacing.unit * 2, - }, - - formControl: { - minWidth: '100%', - }, }); export default withStyles(styles)(Create); diff --git a/resources/js/views/__backoffice/users/Forms/Account.js b/resources/js/views/__backoffice/users/Forms/Account.js new file mode 100644 index 0000000..39bf10a --- /dev/null +++ b/resources/js/views/__backoffice/users/Forms/Account.js @@ -0,0 +1,202 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Formik, Form } from 'formik'; +import * as Yup from 'yup'; + +import { + Button, + FormControl, + FormHelperText, + Input, + Grid, + InputLabel, + MenuItem, + Select, + Typography, + withStyles, +} from '@material-ui/core'; + +const Account = props => { + const { + classes, + values, + errors: apiErrors, + handleSubmit, + handleBack, + } = props; + + return ( + + {({ values, handleChange, errors, isSubmitting }) => { + if (apiErrors && Object.keys(apiErrors).length > 0) { + errors = apiErrors; + } + + return ( +
+ + Account Settings + + + + + + + Type{' '} + + * + + + + + + {errors.hasOwnProperty('type') && ( + + {errors.type} + + )} + + + + + + + + + Email{' '} + + * + + + + + {errors.hasOwnProperty('email') && ( + + {errors.email} + + )} + + + + + + + Username + + + + {errors.hasOwnProperty('username') && ( + + {errors.username} + + )} + + + + +
+ + + + + + + + + + ); + }} + + ); +}; + +Account.propTypes = { + values: PropTypes.object.isRequired, + errors: PropTypes.object, + handleSubmit: PropTypes.func.isRequired, +}; + +const styles = theme => ({ + sectionSpacer: { + marginTop: theme.spacing.unit * 2, + }, + + formControl: { + minWidth: '100%', + }, + + required: { + color: theme.palette.error.main, + }, + + backButton: { + marginRight: theme.spacing.unit, + }, +}); + +export default withStyles(styles)(Account); diff --git a/resources/js/views/__backoffice/users/Forms/Avatar.js b/resources/js/views/__backoffice/users/Forms/Avatar.js new file mode 100644 index 0000000..a2359b5 --- /dev/null +++ b/resources/js/views/__backoffice/users/Forms/Avatar.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Button, Grid, Typography, withStyles } from '@material-ui/core'; + +const Avatar = props => { + const { classes, values, errors, handleSubmit, handleSkip } = props; + + return ( + <> + + Avatar Upload + + +
+ +
+ + + + + + + + ); +}; + +Avatar.propTypes = { + values: PropTypes.object.isRequired, + errors: PropTypes.object, + handleSubmit: PropTypes.func.isRequired, + handleSkip: PropTypes.func.isRequired, +}; + +const styles = theme => ({ + root: { + padding: theme.spacing.unit * 10, + border: `1px solid ${theme.palette.text.primary}`, + }, + + sectionSpacer: { + marginTop: theme.spacing.unit * 2, + }, +}); + +export default withStyles(styles)(Avatar); diff --git a/resources/js/views/__backoffice/users/Forms/Profile.js b/resources/js/views/__backoffice/users/Forms/Profile.js new file mode 100644 index 0000000..4fc5b4d --- /dev/null +++ b/resources/js/views/__backoffice/users/Forms/Profile.js @@ -0,0 +1,308 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Formik, Form } from 'formik'; +import * as Yup from 'yup'; + +import { + Button, + FormControl, + FormHelperText, + Input, + Grid, + InputLabel, + MenuItem, + Select, + Typography, + withStyles, +} from '@material-ui/core'; + +import { MuiPickersUtilsProvider, DatePicker } from 'material-ui-pickers'; +import MomentUtils from '@date-io/moment'; + +const Profile = props => { + const { classes, values, errors: apiErrors, handleSubmit } = props; + + return ( + { + let mappedValues = {}; + let valuesArray = Object.values(values); + + // Format values specially the object ones (i.e Moment) + Object.keys(values).forEach((filter, key) => { + if ( + valuesArray[key] !== null && + typeof valuesArray[key] === 'object' && + valuesArray[key].hasOwnProperty('_isAMomentObject') + ) { + mappedValues[filter] = moment(valuesArray[key]).format( + 'YYYY-MM-DD', + ); + + return; + } + + mappedValues[filter] = valuesArray[key]; + }); + + await handleSubmit(mappedValues, form); + }} + validateOnBlur={false} + > + {({ + values, + handleChange, + setFieldValue, + errors, + isSubmitting, + }) => { + if (apiErrors && Object.keys(apiErrors).length > 0) { + errors = apiErrors; + } + + return ( +
+ + Personal Information + + + + + + + Firstname{' '} + + * + + + + + + {errors.hasOwnProperty('firstname') && ( + + {errors.firstname} + + )} + + + + + + + Middlename + + + + + {errors.hasOwnProperty('middlename') && ( + + {errors.middlename} + + )} + + + + + + + Lastname{' '} + + * + + + + + + {errors.hasOwnProperty('lastname') && ( + + {errors.lastname} + + )} + + + + + + + + + Gender + + + + + {errors.hasOwnProperty('gender') && ( + + {errors.gender} + + )} + + + + + + + + setFieldValue('birthdate', date) + } + format="YYYY-MM-DD" + maxDate={moment() + .subtract(10, 'y') + .subtract(1, 'd') + .format('YYYY-MM-DD')} + keyboard + clearable + disableFuture + /> + + + {errors.hasOwnProperty('birthdate') && ( + + {errors.birthdate} + + )} + + + + + + + + + Address + + + + + {errors.hasOwnProperty('address') && ( + + {errors.address} + + )} + + + + +
+ + + + + + + + ); + }} + + ); +}; + +Profile.propTypes = { + values: PropTypes.object.isRequired, + errors: PropTypes.object, + handleSubmit: PropTypes.func.isRequired, +}; + +const styles = theme => ({ + formControl: { + minWidth: '100%', + }, + + required: { + color: theme.palette.error.main, + }, +}); + +export default withStyles(styles)(Profile); diff --git a/resources/js/views/__backoffice/users/Forms/index.js b/resources/js/views/__backoffice/users/Forms/index.js new file mode 100644 index 0000000..43f9436 --- /dev/null +++ b/resources/js/views/__backoffice/users/Forms/index.js @@ -0,0 +1,5 @@ +import loadable from '@loadable/component'; + +export const Account = loadable(() => import('./Account')); +export const Avatar = loadable(() => import('./Avatar')); +export const Profile = loadable(() => import('./Profile'));