diff --git a/.eslintrc b/.eslintrc index 87e8323..43b7592 100644 --- a/.eslintrc +++ b/.eslintrc @@ -192,7 +192,6 @@ ], "react/destructuring-assignment": "off", "react/forbid-component-props": "off", - "react/forbid-prop-types": "off", "react/jsx-handler-names": "off", "react/jsx-indent-props": "off", "react/jsx-max-depth": "off", diff --git a/CHANGELOG.md b/CHANGELOG.md index ee8ce35..9fbf0e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +# [v7.0.0](https://github.com/neonexus/sails-react-bootstrap-webpack/compare/v6.1.0...v7.0.0) (2025-01-23) +### Features + +* Updated to React 19. + * Rewrote most (nearly all) custom React elements to work with v19 (due to removal of `PropTypes`). +* Fixed (read: silenced) SASS deprecation warnings in Webpack config. +* Fixed security audits. +* Updated dependencies. +* Set minimum Node version to v22.13. + +### Breaking Changes + +* Because of the upgrade to React 19, there are fairly major changes to the custom React elements. + # [v6.1.0](https://github.com/neonexus/sails-react-bootstrap-webpack/compare/v6.0.1...v6.1.0) (2024-11-18) ### Features @@ -17,10 +31,13 @@ # [v6.0.0](https://github.com/neonexus/sails-react-bootstrap-webpack/compare/v5.3.4...v6.0.0) (2024-11-06) ### Features -* Removed unneeded index.jsx. * Made self-update a little smarter. * Updated dependencies. +### Breaking Changes + +* Removed unneeded index.jsx. + # [v5.3.4](https://github.com/neonexus/sails-react-bootstrap-webpack/compare/v5.3.3...v5.3.4) (2024-05-03) ### Features diff --git a/README.md b/README.md index 8d2cfb6..d706256 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # sails-react-bootstrap-webpack -[![Sails version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fneonexus%2Fsails-react-bootstrap-webpack%2Fv6.1.0%2Fpackage.json&query=%24.dependencies.sails&label=Sails&logo=sailsdotjs)](https://sailsjs.com) -[![React version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fneonexus%2Fsails-react-bootstrap-webpack%2Fv6.1.0%2Fpackage.json&query=%24.devDependencies.react&label=React&logo=react)](https://react.dev) -[![Bootstrap version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fneonexus%2Fsails-react-bootstrap-webpack%2Fv6.1.0%2Fpackage.json&query=%24.devDependencies.bootstrap&label=Bootstrap&logo=bootstrap&logoColor=white)](https://getbootstrap.com) -[![Webpack version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fneonexus%2Fsails-react-bootstrap-webpack%2Fv6.1.0%2Fpackage.json&query=%24.devDependencies.webpack&label=Webpack&logo=webpack)](https://webpack.js.org) +[![Sails version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fneonexus%2Fsails-react-bootstrap-webpack%2Fv7.0.0%2Fpackage.json&query=%24.dependencies.sails&label=Sails&logo=sailsdotjs)](https://sailsjs.com) +[![React version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fneonexus%2Fsails-react-bootstrap-webpack%2Fv7.0.0%2Fpackage.json&query=%24.devDependencies.react&label=React&logo=react)](https://react.dev) +[![Bootstrap version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fneonexus%2Fsails-react-bootstrap-webpack%2Fv7.0.0%2Fpackage.json&query=%24.devDependencies.bootstrap&label=Bootstrap&logo=bootstrap&logoColor=white)](https://getbootstrap.com) +[![Webpack version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fneonexus%2Fsails-react-bootstrap-webpack%2Fv7.0.0%2Fpackage.json&query=%24.devDependencies.webpack&label=Webpack&logo=webpack)](https://webpack.js.org) Latest version only: [![FOSSA License Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fneonexus%2Fsails-react-bootstrap-webpack.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fneonexus%2Fsails-react-bootstrap-webpack?ref=badge_shield) @@ -21,9 +21,13 @@ A virtual start-up in a box! NOTE: You will need access to a MySQL / MariaDB database for the quick setup. If you want to use a different datastore, you'll need to configure it manually. -[Aiven.io](https://aiven.io) has FREE (no CC required) secure MySQL (5 GB), and Redis (1 GB). Both require use of SSL, and can be restricted to specified IPs. (If you are having trouble finding the -FREE instances, you need to select Digital Ocean as the cloud provider.) Use my [referral link](https://console.aiven.io/signup?referral_code=mk36ekt3wo1dvij7joon) to signup, and you'll get $100 +
+ For a free / quick option, check out... + +[Aiven.io](https://aiven.io) has a no credit card required, secure MySQL (5 GB), and Redis (1 GB). Both require use of SSL, and can be restricted to specified IPs. (If you are having trouble finding +the FREE instances, you need to select Digital Ocean as the cloud provider.) Use my [referral link](https://console.aiven.io/signup?referral_code=mk36ekt3wo1dvij7joon) to signup, and you'll get $100 extra when you start a trial (trial is NOT needed for the free servers). +
```shell npx drfg neonexus/sails-react-bootstrap-webpack my-new-site @@ -100,8 +104,8 @@ The `master` branch is experimental, and the [release branch](https://github.com ## Current Dependencies * [Sails](https://sailsjs.com) **v1** -* [React](https://react.dev) **v18** -* [React Router](https://reactrouter.com) **v6** +* [React](https://react.dev) **v19** +* [React Router](https://reactrouter.com) **v7** * [Bootstrap](https://getbootstrap.com) **v5** * [React-Bootstrap](https://react-bootstrap.github.io) **v2** * [Webpack](https://webpack.js.org) **v5** @@ -697,7 +701,16 @@ Start said service: sudo systemctl start crond.service ``` -Edit the `crontab` to run the script at `@reboot`: +Edit the `crontab` to run the script at `@reboot` (you don't have to use `nano`, I just find it easier): + +Also, notice to lack of `sudo` here. This is because we DON'T want our backend to have super-user powers! That just, would not be good... We want it running under a general user. +For better security, it should be run via a user with limited permissions. + +```shell +EDITOR=nano crontab -e +``` + +Add this: ```shell @reboot cd myapp; ./tmux.sh diff --git a/api/controllers/README.md b/api/controllers/README.md index 8d70c67..659203f 100644 --- a/api/controllers/README.md +++ b/api/controllers/README.md @@ -2,4 +2,4 @@ Here is where all of our API actions live. A controller in this context is a folder, and an action of the controller is an individual file. Each action is using the new "actions2" style, as opposed to the classic. -See: https://sailsjs.com/documentation/concepts/actions-and-controllers +See: https://sailsjs.com/documentation/concepts/actions-and-controllers#?actions-2 diff --git a/api/controllers/admin/create-user.js b/api/controllers/admin/create-user.js index 5a73cf0..065737d 100644 --- a/api/controllers/admin/create-user.js +++ b/api/controllers/admin/create-user.js @@ -40,7 +40,7 @@ module.exports = { generatePassword: { type: 'boolean', defaultsTo: false, - description: 'Used to auto-generate a password for the user' + description: 'Used to auto-generate a password for the user. Negates the `password` input.' } }, diff --git a/assets/src/Admin/AdminRouter.jsx b/assets/src/Admin/AdminRouter.jsx index 2799681..31ff4e0 100644 --- a/assets/src/Admin/AdminRouter.jsx +++ b/assets/src/Admin/AdminRouter.jsx @@ -1,5 +1,4 @@ import {Component, StrictMode, Suspense, lazy} from 'react'; -import PropTypes from 'prop-types'; import '../../styles/admin/admin.scss'; import { Routes, @@ -37,11 +36,6 @@ function RenderOrLogin(props) { return null; // not ready yet } -RenderOrLogin.propTypes = { - api: PropTypes.object.isRequired, - userContext: PropTypes.object.isRequired -}; - const theApi = new api(); class AdminRouter extends Component { diff --git a/assets/src/Admin/Login.jsx b/assets/src/Admin/Login.jsx index 67a4c29..5228c43 100644 --- a/assets/src/Admin/Login.jsx +++ b/assets/src/Admin/Login.jsx @@ -1,5 +1,4 @@ import { Component } from 'react'; -import PropTypes from 'prop-types'; import {UserConsumer} from '../data/UserContext'; import defaultAPIErrorHandler from '../data/defaultAPIErrorHandler'; @@ -10,6 +9,10 @@ class Login extends Component { constructor(props) { super(props); + if (!props.api) { + throw new Error('No API passed to Login.jsx.'); + } + this.state = { email: localStorage.getItem('email') || '', password: '', @@ -155,8 +158,4 @@ class Login extends Component { } } -Login.propTypes = { - api: PropTypes.object.isRequired -}; - export default Login; diff --git a/assets/src/Admin/NavBar.jsx b/assets/src/Admin/NavBar.jsx index 2bd09c3..a0238b7 100644 --- a/assets/src/Admin/NavBar.jsx +++ b/assets/src/Admin/NavBar.jsx @@ -1,6 +1,4 @@ import {useState, useContext, useEffect} from 'react'; -import PropTypes from 'prop-types'; - import {Button, Nav, Navbar, NavDropdown} from 'react-bootstrap'; import {NavLink as ReactNavLink} from 'react-router-dom'; @@ -23,7 +21,13 @@ function scrollListener() { } } -function NavBar(props) { +function NavBar({handleLogout = null}) { + if (typeof handleLogout !== 'function') { + console.error('`handleLogout` is a required function for NavBar.'); + + return 'ERROR!'; + } + const [isExpanded, setIsExpanded] = useState(false); const user = useContext(UserContext); @@ -110,7 +114,7 @@ function NavBar(props) { user.isLoggedIn ? <> Welcome, {user.info.firstName}     - + : null } @@ -125,8 +129,4 @@ function NavBar(props) { ); } -NavBar.propTypes = { - handleLogout: PropTypes.func.isRequired -}; - export default NavBar; diff --git a/assets/src/Admin/Settings/ChangePasswordModal.jsx b/assets/src/Admin/Settings/ChangePasswordModal.jsx index 953d86a..a2cd410 100644 --- a/assets/src/Admin/Settings/ChangePasswordModal.jsx +++ b/assets/src/Admin/Settings/ChangePasswordModal.jsx @@ -1,16 +1,38 @@ -import { useState } from 'react'; -import PropTypes from 'prop-types'; - +import {useState} from 'react'; import {Button, Form, Modal, Spinner} from 'react-bootstrap'; -function ChangePasswordModal(props) { +/** + * Change Password Modal + * + * @param {object} api + * @param {function} onCancel + * @param {function} onUpdate + * @param {boolean} show + * + * @returns {JSX.Element} + */ +function ChangePasswordModal({api, onCancel, onUpdate, show}) { + if (api === undefined || onCancel === undefined || onUpdate === undefined || show === undefined) { + throw new Error('`api`, `onCancel`, `onUpdate`, and `show` are required parameters for ChangePasswordModal.'); + } + + if (typeof onCancel !== 'function') { + throw new Error('`onCancel` must be a function in ChangePasswordModal.'); + } + + if (typeof onUpdate !== 'function') { + throw new Error('`onUpdate` must be a function in ChangePasswordModal.'); + } + + if (typeof show !== 'boolean') { + throw new Error('`show` must be a boolean in ChangePasswordModal.'); + } + const [isLoading, setIsLoading] = useState(false); const [current, setCurrent] = useState(''); const [newPass, setNewPass] = useState(''); const [confPass, setConfPass] = useState(''); - - function resetState() { setCurrent(''); setNewPass(''); @@ -18,16 +40,16 @@ function ChangePasswordModal(props) { setIsLoading(false); } - function onCancel() { + function onCancelHandler() { resetState(); - props.onCancel(); + onCancel(); } - function onUpdate() { + function onUpdateHandler() { setIsLoading(true); - props.api.post({ + api.post({ url: '/password', body: { currentPassword: current, @@ -37,7 +59,7 @@ function ChangePasswordModal(props) { }, (body) => { setIsLoading(false); alert('Password updated successfully.'); - props.onUpdate(); + onUpdate(); resetState(); }, (err, body) => { console.error(err); @@ -47,8 +69,8 @@ function ChangePasswordModal(props) { } return ( - -
{e.preventDefault(); onUpdate();}}> + + {e.preventDefault(); onUpdateHandler();}}> Change Password @@ -68,7 +90,7 @@ function ChangePasswordModal(props) { - + + ); } -Disable2FAModal.propTypes = { - api: PropTypes.object.isRequired, - onCancel: PropTypes.func.isRequired, - onDisable: PropTypes.func.isRequired, - show: PropTypes.bool.isRequired -}; - export default Disable2FAModal; diff --git a/assets/src/Admin/Settings/Enable2FAModal.jsx b/assets/src/Admin/Settings/Enable2FAModal.jsx index 62198e0..5356542 100644 --- a/assets/src/Admin/Settings/Enable2FAModal.jsx +++ b/assets/src/Admin/Settings/Enable2FAModal.jsx @@ -1,8 +1,20 @@ import {useState} from 'react'; -import PropTypes from 'prop-types'; import {Button, Collapse, Fade, Form, Modal} from 'react-bootstrap'; -function Enable2FAModal(props) { +function Enable2FAModal({api, onCancel, onSuccess, show}) { + if (typeof api !== 'object' || api === null) { + throw new Error('Invalid or missing `api` object.'); + } + if (typeof onCancel !== 'function') { + throw new Error('Invalid or missing `onCancel` function.'); + } + if (typeof onSuccess !== 'function') { + throw new Error('Invalid or missing `onSuccess` function.'); + } + if (typeof show !== 'boolean') { + throw new Error('Invalid or missing `show` boolean.'); + } + const [isLoading, setIsLoading] = useState(false); const [currentPage, setCurrentPage] = useState(0); const [showCode, setShowCode] = useState(false); @@ -23,13 +35,13 @@ function Enable2FAModal(props) { setIsLoading(false); } - function onCancel() { - props.onCancel(); + function handleCancel() { + onCancel(); resetState(); } function getCode() { - props.api.post({ + api.post({ url: '/2fa/enable' }, (body) => { setQR(body.image); @@ -43,7 +55,7 @@ function Enable2FAModal(props) { } function validateCode() { - props.api.post({ + api.post({ url: '/2fa/finalize', body: { password, @@ -83,7 +95,7 @@ function Enable2FAModal(props) { break; case 3: if (confirm('Are you sure you have saved your backup codes? They won\'t be shown again.')) { - props.onSuccess(); + onSuccess(); resetState(); } break; @@ -96,7 +108,7 @@ function Enable2FAModal(props) { } return ( - + Enable 2-Factor Authentication @@ -189,7 +201,7 @@ function Enable2FAModal(props) { - + @@ -197,11 +209,4 @@ function Enable2FAModal(props) { ); } -Enable2FAModal.propTypes = { - api: PropTypes.object.isRequired, - onCancel: PropTypes.func.isRequired, - onSuccess: PropTypes.func.isRequired, - show: PropTypes.bool.isRequired -}; - export default Enable2FAModal; diff --git a/assets/src/Admin/Settings/SecuritySection.jsx b/assets/src/Admin/Settings/SecuritySection.jsx index 0cbaaaa..eb20d93 100644 --- a/assets/src/Admin/Settings/SecuritySection.jsx +++ b/assets/src/Admin/Settings/SecuritySection.jsx @@ -3,29 +3,40 @@ import {useContext, useState} from 'react'; import UserContext from '../../data/UserContext'; import ChangePasswordModal from './ChangePasswordModal'; import Enable2FAModal from './Enable2FAModal'; -import PropTypes from 'prop-types'; import Disable2FAModal from './Disable2FAModal'; -function SecuritySection(props) { +function SecuritySection({api}) { + if (!api || typeof api !== 'object') { + throw new Error('Invalid or missing `api` prop. Expected an object.'); + } + const user = useContext(UserContext); const [showChangePassword, setShowChangePassword] = useState(false); const [showEnableOTP, setShowEnableOTP] = useState(false); const [showDisableOTP, setShowDisableOTP] = useState(false); function handleEnableOTP() { - user.login(_.merge({}, user.info, {_isOTPEnabled: true})); - setShowEnableOTP(false); + if (user && user.login && user.info) { + user.login({ ...user.info, _isOTPEnabled: true }); + setShowEnableOTP(false); + } else { + console.error('User context is invalid or missing required properties.'); + } } function handleDisableOTP() { - user.login(_.merge({}, user.info, {_isOTPEnabled: false})); - setShowDisableOTP(false); + if (user && user.login && user.info) { + user.login({ ...user.info, _isOTPEnabled: false }); + setShowDisableOTP(false); + } else { + console.error('User context is invalid or missing required properties.'); + } } return ( <>

Security Settings

- +

Change Password

Always a good idea to rotate your passwords.

@@ -34,46 +45,20 @@ function SecuritySection(props) {
- + -

- 2-Factor Authentication - { - (user.info._isOTPEnabled) - ? Enabled - : Disabled - } -

- { - (user.info._isOTPEnabled) - ?

Nice! Your account security is even stronger, because 2FA is enabled.

- :

It is highly recommended to enable 2FA to increase security with your account.

- } - - { - // (user.info._isOTPEnabled) - // ? - // : null - } +

2-Factor Authentication {user.info._isOTPEnabled ? Enabled : Disabled}

+ {user.info._isOTPEnabled ?

Nice! Your account security is even stronger, because 2FA is enabled.

:

It is highly recommended to enable 2FA to increase security with your account.

} - { - (user.info._isOTPEnabled) - ? - : - } + {user.info._isOTPEnabled ? : }
- - setShowChangePassword(false)} onUpdate={() => setShowChangePassword(false)} /> - setShowEnableOTP(false)} onSuccess={handleEnableOTP} /> - setShowDisableOTP(false)} onDisable={handleDisableOTP} /> + setShowChangePassword(false)} onUpdate={() => setShowChangePassword(false)} /> + setShowEnableOTP(false)} onSuccess={handleEnableOTP} /> + setShowDisableOTP(false)} onDisable={handleDisableOTP} /> ); } -SecuritySection.propTypes = { - api: PropTypes.object.isRequired -}; - export default SecuritySection; diff --git a/assets/src/Admin/Settings/Settings.jsx b/assets/src/Admin/Settings/Settings.jsx index 2df001a..cce82bd 100644 --- a/assets/src/Admin/Settings/Settings.jsx +++ b/assets/src/Admin/Settings/Settings.jsx @@ -1,13 +1,11 @@ import {useState, lazy, useEffect, Suspense} from 'react'; -import PropTypes from 'prop-types'; - import {Nav, Spinner, TabContainer, TabContent, TabPane} from 'react-bootstrap'; import {NavLink, Route, Routes, useLocation} from 'react-router-dom'; const ProfileSection = lazy(() => import('./ProfileSection')); const SecuritySection = lazy(() => import('./SecuritySection')); -function Settings(props) { +function Settings({api}) { const {pathname} = useLocation(); const [currentPath, setCurrentPath] = useState(pathname); @@ -29,7 +27,7 @@ function Settings(props) { name: 'Security', key: 'security', url: '/admin/settings/security', - el: + el: } ]; @@ -69,8 +67,4 @@ function Settings(props) { ); } -Settings.propTypes = { - api: PropTypes.object.isRequired -}; - export default Settings; diff --git a/assets/src/Admin/Users/CreateUserModal.jsx b/assets/src/Admin/Users/CreateUserModal.jsx index f940f8b..01e2d81 100644 --- a/assets/src/Admin/Users/CreateUserModal.jsx +++ b/assets/src/Admin/Users/CreateUserModal.jsx @@ -1,229 +1,199 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, {useState, useCallback} from 'react'; import {Button, Collapse, Form, Modal, Row} from 'react-bootstrap'; -class CreateUserModal extends Component { - constructor(props) { - super(props); - - this.state = { - generatePassword: true, - wasValidated: false, - firstName: '', - lastName: '', - email: '', - role: 'user', - password1: '', - password2: '', - isDuplicateEmail: false, - doPasswordsMatch: true, - isLoading: false - }; - - this.handleClose = this.handleClose.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handlePasswordMatching = this.handlePasswordMatching.bind(this); - } - - handleSubmit(e) { +const initialState = { + generatePassword: true, + wasValidated: false, + firstName: '', + lastName: '', + email: '', + role: 'user', + password1: '', + password2: '', + isDuplicateEmail: false, + doPasswordsMatch: true, + isLoading: false +}; + +const CreateUserModal = ({ show, onClose, onCreate }) => { + const [state, setState] = useState(initialState); + + const handleClose = useCallback(() => { + setState(initialState); + if (onClose) onClose(); + }, [onClose]); + + const handlePasswordMatching = useCallback(() => { + setState((prevState) => ({ + ...prevState, + doPasswordsMatch: prevState.password1 === prevState.password2 + })); + }, []); + + const handleSubmit = useCallback((e) => { e.preventDefault(); e.stopPropagation(); - this.setState({wasValidated: true, isLoading: true}); + setState((prevState) => ({ ...prevState, wasValidated: true, isLoading: true })); + + const isValidPassword = + state.generatePassword || + (state.password1 === state.password2 && state.password1.length > 5); - if (e.target.checkValidity() && (this.state.generatePassword || (this.state.password1 === this.state.password2 && this.state.password1.length > 5))) { - this.props.onCreate( + if (e.target.checkValidity() && isValidPassword) { + onCreate( { - firstName: this.state.firstName, - lastName: this.state.lastName, - email: this.state.email, - role: this.state.role, - generatePassword: this.state.generatePassword, - password: this.state.password1 + firstName: state.firstName, + lastName: state.lastName, + email: state.email, + role: state.role, + generatePassword: state.generatePassword, + password: state.password1 }, - this.handleClose, - () => this.setState({isLoading: false}) + handleClose, + () => setState((prevState) => ({ ...prevState, isLoading: false })) ); } else { - this.setState({isLoading: false}); - } - } - - handleClose(onClose) { - if (!onClose) { - onClose = _.noop; + setState((prevState) => ({ ...prevState, isLoading: false })); } + }, [state, onCreate, handleClose]); + + return ( + + + Create New User + + + + + + + First Name + setState((prevState) => ({ ...prevState, firstName: e.target.value }))} + required + autoFocus + disabled={state.isLoading} + /> + First name is required. + - this.setState({ - firstName: '', - lastName: '', - email: '', - role: 'user', - password1: '', - password2: '', - generatePassword: true, - wasValidated: false, - isLoading: false - }, onClose); - } - - handlePasswordMatching() { - if (this.state.doPasswordsMatch && this.state.password1 !== this.state.password2) { - this.setState({doPasswordsMatch: false}); - } else if (!this.state.doPasswordsMatch && this.state.password1 === this.state.password2) { - this.setState({doPasswordsMatch: true}); - } - } - - render() { - return ( - this.handleClose(this.props.onClose)} backdrop="static"> - - - Create New User - - - - this.handleSubmit(e)} validated={this.state.wasValidated} noValidate> - - - - First Name + + Last Name + setState((prevState) => ({ ...prevState, lastName: e.target.value }))} + required + disabled={state.isLoading} + /> + Last name is required. + + + + + Email address + setState((prevState) => ({ ...prevState, email: e.target.value }))} + required + disabled={state.isLoading} + /> + + {state.isDuplicateEmail ? 'Email is already in-use.' : 'Email address is required.'} + + + + + Role + setState((prevState) => ({ ...prevState, role: e.target.value }))} + disabled={state.isLoading} + > + + + + + + + setState((prevState) => ({ ...prevState, generatePassword: !e.target.checked }))} + disabled={state.isLoading} + /> + + + +
+ + Password this.setState({firstName: e.target.value})} + type="password" + placeholder="Enter Password" + value={state.password1} + onChange={(e) => setState((prevState) => ({ ...prevState, password1: e.target.value }))} + className={ + state.wasValidated && state.password1.length < 6 ? 'is-invalid' : '' + } required - autoFocus - disabled={this.state.isLoading} + disabled={state.isLoading || state.generatePassword} + minLength="6" + maxLength="72" /> - First name is required. + + {state.password1.length + ? 'Password is too short.' + : 'Password is required.'} + - - Last Name + + Verify Password this.setState({lastName: e.target.value})} + type="password" + placeholder="Verify Password" + value={state.password2} + onChange={(e) => setState((prevState) => ({ ...prevState, password2: e.target.value }))} + className={ + state.wasValidated && + state.password1 !== state.password2 && + state.password2.length + ? 'is-invalid' + : '' + } required - disabled={this.state.isLoading} + disabled={state.isLoading || state.generatePassword} + minLength="6" /> - Last name is required. + + Passwords do not match. + - - - - Email address - this.setState({email: e.target.value})} - required - disabled={this.state.isLoading} - /> - - { - (this.state.isDuplicateEmail) ? 'Email is already in-use.' : 'Email address is required.' - } - - - - - Role - this.setState({role: e.target.value})} - disabled={this.state.isLoading} - > - - - - - - - this.setState({generatePassword: !e.target.checked})} - className="prevent-validation" - disabled={this.state.isLoading} - /> - - - -
- - Password - this.setState({password1: e.target.value})} - className={ - (this.state.wasValidated && this.state.password1.length < 6) - ? 'is-invalid' - : null - } - required - disabled={this.state.isLoading || this.state.generatePassword} - minLength="6" - maxLength="72" - /> - 5) ? 'd-none' : null}> - { - (this.state.password1.length) - ? 'Password is too short.' - : 'Password is required.' - } - - - - - Verify Password - this.setState({password2: e.target.value})} - className={ - (this.state.wasValidated && this.state.password1 !== this.state.password2 && this.state.password2.length) - ? 'is-invalid' - : null - } - required - disabled={this.state.isLoading || this.state.generatePassword} - minLength="6" - /> - { - (this.state.wasValidated && this.state.password1.length && this.state.password2.length) - ? Passwords do not match. - : null - } - -
-
- - - - - - - - - ); - } -} - -CreateUserModal.propTypes = { - onClose: PropTypes.func.isRequired, - onCreate: PropTypes.func.isRequired, - show: PropTypes.bool.isRequired +
+
+
+ + + + + + +
+ ); }; export default CreateUserModal; diff --git a/assets/src/Admin/Users/DeleteUserModal.jsx b/assets/src/Admin/Users/DeleteUserModal.jsx index dd37d5f..9fab7c7 100644 --- a/assets/src/Admin/Users/DeleteUserModal.jsx +++ b/assets/src/Admin/Users/DeleteUserModal.jsx @@ -1,15 +1,22 @@ -import { useState } from 'react'; -import PropTypes from 'prop-types'; +import {useState} from 'react'; import {Button, Modal} from 'react-bootstrap'; -function DeleteUserModal(props) { +function DeleteUserModal({ + firstName = '', + lastName = '', + onAccept = () => {}, + onCancel = () => {}, + role = '', + show = false, + softDelete = true +}) { const [isLoading, setIsLoading] = useState(false); return ( - + - {props.softDelete ? 'Delete User' : 'PERMANENTLY Delete User'} + {softDelete ? 'Delete User' : 'PERMANENTLY Delete User'} @@ -17,20 +24,20 @@ function DeleteUserModal(props) { Are you sure you want to delete this user?
{ - props.softDelete + softDelete ?
(This action can be undone later)
:
(This is a PERMANENT action, and CAN NOT be undone!)
}
- {props.firstName} {props.lastName} ({props.role}) + {firstName} {lastName} ({role})
- +