From 1ab48e82fed6a140e002c8c3eaa13daf3ec7622e Mon Sep 17 00:00:00 2001 From: jttps Date: Thu, 27 Aug 2020 18:05:58 +1000 Subject: [PATCH 1/6] Added react-moment for parsing date time --- package-lock.json | 24 +++++++++++++++++++++--- package.json | 3 +++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 92a7096..672131e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4744,9 +4744,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash-es": { "version": "4.17.15", @@ -5022,6 +5022,19 @@ "minimist": "^1.2.5" } }, + "moment": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", + "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" + }, + "moment-timezone": { + "version": "0.5.31", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz", + "integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==", + "requires": { + "moment": ">= 2.9.0" + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -6079,6 +6092,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-moment": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/react-moment/-/react-moment-0.9.7.tgz", + "integrity": "sha512-ifzUrUGF6KRsUN2pRG5k56kO0mJBr8kRkWb0wNvtFIsBIxOuPxhUpL1YlXwpbQCbHq23hUu6A0VEk64HsFxk9g==" + }, "react-router": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", diff --git a/package.json b/package.json index 627d30f..5958b74 100755 --- a/package.json +++ b/package.json @@ -13,10 +13,13 @@ "dependencies": { "formik": "^2.1.4", "history": "^4.10.1", + "moment": "^2.27.0", + "moment-timezone": "^0.5.31", "prop-types": "^15.7.2", "query-string": "^6.11.0", "react": "^16.8.6", "react-dom": "^16.8.6", + "react-moment": "^0.9.7", "react-router-dom": "^5.0.0", "rxjs": "^6.3.3", "yup": "^0.28.1" From 648228d5e956fa8f1becf3d7a8716a82ef910fda Mon Sep 17 00:00:00 2001 From: jttps Date: Thu, 27 Aug 2020 18:26:21 +1000 Subject: [PATCH 2/6] Added utilities service --- src/_services/index.js | 1 + src/_services/utilities.service.js | 57 ++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/_services/utilities.service.js diff --git a/src/_services/index.js b/src/_services/index.js index b4fa8ab..4d51f4b 100644 --- a/src/_services/index.js +++ b/src/_services/index.js @@ -1,2 +1,3 @@ export * from './account.service'; export * from './alert.service'; +export * from './utilities.service'; diff --git a/src/_services/utilities.service.js b/src/_services/utilities.service.js new file mode 100644 index 0000000..6fb93dc --- /dev/null +++ b/src/_services/utilities.service.js @@ -0,0 +1,57 @@ +import { BehaviorSubject } from 'rxjs'; + +import config from 'config'; +import { fetchWrapper } from '@/_helpers'; + +const utilitySubject = new BehaviorSubject(null); +const baseUrl = `${config.apiUrl}/utilities`; + +export const utilitiesService = { + enable, + disable, + getAll, + getById, + create, + update, + delete: _delete, + user: utilitySubject.asObservable(), + get utilityValue () { return utilitySubject.value } +}; + +function enable() { + return fetchWrapper.get(`${baseUrl}/enable`); +} + +function disable() { + return fetchWrapper.get(`${baseUrl}/disable`); +} + +function getAll() { + return fetchWrapper.get(baseUrl); +} + +function getById(id) { + return fetchWrapper.get(`${baseUrl}/${id}`); +} + +function create(params) { + return fetchWrapper.post(baseUrl, params); +} + +function update(id, params) { + return fetchWrapper.put(`${baseUrl}/${id}`, params) + .then(utility => { + // update stored user if the logged in user updated their own record + if (utility.id === utilitySubject.value.id) { + // publish updated user to subscribers + utility = { ...utilitySubject.value, ...utility }; + utilitySubject.next(utility); + } + return utility; + }); +} + +// prefixed with underscore because 'delete' is a reserved word in javascript +function _delete(id) { + return fetchWrapper.delete(`${baseUrl}/${id}`) +} From b9a22de0f4a0e38a5fb99a14a6b7ef55d6b32e73 Mon Sep 17 00:00:00 2001 From: jttps Date: Thu, 27 Aug 2020 19:22:02 +1000 Subject: [PATCH 3/6] Added utilities front-end to list utilities --- src/admin/Index.jsx | 2 + src/admin/Overview.jsx | 5 +- src/admin/utilities/AddEdit.jsx | 164 ++++++++++++++++++++++++++++++++ src/admin/utilities/Index.jsx | 19 ++++ src/admin/utilities/List.jsx | 79 +++++++++++++++ 5 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 src/admin/utilities/AddEdit.jsx create mode 100644 src/admin/utilities/Index.jsx create mode 100644 src/admin/utilities/List.jsx diff --git a/src/admin/Index.jsx b/src/admin/Index.jsx index e352597..ee264b7 100644 --- a/src/admin/Index.jsx +++ b/src/admin/Index.jsx @@ -3,6 +3,7 @@ import { Route, Switch } from 'react-router-dom'; import { Overview } from './Overview'; import { Users } from './users'; +import { Utilities } from './utilities'; function Admin({ match }) { const { path } = match; @@ -13,6 +14,7 @@ function Admin({ match }) { + diff --git a/src/admin/Overview.jsx b/src/admin/Overview.jsx index ed0bdcd..2b4212a 100644 --- a/src/admin/Overview.jsx +++ b/src/admin/Overview.jsx @@ -8,7 +8,10 @@ function Overview({ match }) {

Admin

This section can only be accessed by administrators.

-

Manage Users

+

+ Manage Users + Manage Utilities +

); } diff --git a/src/admin/utilities/AddEdit.jsx b/src/admin/utilities/AddEdit.jsx new file mode 100644 index 0000000..8bd71b3 --- /dev/null +++ b/src/admin/utilities/AddEdit.jsx @@ -0,0 +1,164 @@ +import React, { useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { Formik, Field, Form, ErrorMessage } from 'formik'; +import * as Yup from 'yup'; + +import { accountService, alertService } from '@/_services'; + +function AddEdit({ history, match }) { + const { id } = match.params; + const isAddMode = !id; + + const initialValues = { + title: '', + firstName: '', + lastName: '', + email: '', + role: '', + password: '', + confirmPassword: '' + }; + + const validationSchema = Yup.object().shape({ + title: Yup.string() + .required('Title is required'), + firstName: Yup.string() + .required('First Name is required'), + lastName: Yup.string() + .required('Last Name is required'), + email: Yup.string() + .email('Email is invalid') + .required('Email is required'), + role: Yup.string() + .required('Role is required'), + password: Yup.string() + .concat(isAddMode ? Yup.string().required('Password is required') : null) + .min(6, 'Password must be at least 6 characters'), + confirmPassword: Yup.string() + .when('password', (password, schema) => { + if (password) return schema.required('Confirm Password is required'); + }) + .oneOf([Yup.ref('password')], 'Passwords must match') + }); + + function onSubmit(fields, { setStatus, setSubmitting }) { + setStatus(); + if (isAddMode) { + createUser(fields, setSubmitting); + } else { + updateUser(id, fields, setSubmitting); + } + } + + function createUser(fields, setSubmitting) { + accountService.create(fields) + .then(() => { + alertService.success('User added successfully', { keepAfterRouteChange: true }); + history.push('.'); + }) + .catch(error => { + setSubmitting(false); + alertService.error(error); + }); + } + + function updateUser(id, fields, setSubmitting) { + accountService.update(id, fields) + .then(() => { + alertService.success('Update successful', { keepAfterRouteChange: true }); + history.push('..'); + }) + .catch(error => { + setSubmitting(false); + alertService.error(error); + }); + } + + return ( + + {({ errors, touched, isSubmitting, setFieldValue }) => { + useEffect(() => { + if (!isAddMode) { + // get user and set form fields + accountService.getById(id).then(user => { + const fields = ['title', 'firstName', 'lastName', 'email', 'role']; + fields.forEach(field => setFieldValue(field, user[field], false)); + }); + } + }, []); + + return ( +
+

{isAddMode ? 'Add User' : 'Edit User'}

+
+
+ + + + + + + + + +
+
+ + + +
+
+ + + +
+
+
+
+ + + +
+
+ + + + + + + +
+
+ {!isAddMode && +
+

Change Password

+

Leave blank to keep the same password

+
+ } +
+
+ + + +
+
+ + + +
+
+
+ + Cancel +
+
+ ); + }} +
+ ); +} + +export { AddEdit }; \ No newline at end of file diff --git a/src/admin/utilities/Index.jsx b/src/admin/utilities/Index.jsx new file mode 100644 index 0000000..66417ca --- /dev/null +++ b/src/admin/utilities/Index.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { List } from './List'; +import { AddEdit } from './AddEdit'; + +function Utilities({ match }) { + const { path } = match; + + return ( + + + + + + ); +} + +export { Utilities }; \ No newline at end of file diff --git a/src/admin/utilities/List.jsx b/src/admin/utilities/List.jsx new file mode 100644 index 0000000..2ae977d --- /dev/null +++ b/src/admin/utilities/List.jsx @@ -0,0 +1,79 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import Moment from 'react-moment'; + +import { utilitiesService } from '@/_services'; + +function List({ match }) { + const { path } = match; + const [utilities, setUtilities] = useState(null); + + useEffect(() => { + utilitiesService.getAll().then(x => setUtilities(x)); + }, []); + + function deleteUtility(id) { + setUtilities(utilities.map(x => { + if (x.id === id) { x.isDeleting = true; } + return x; + })); + utilitiesService.delete(id).then(() => { + setUtilities(utilities => utilities.filter(x => x.id !== id)); + }); + } + + function utilityStatus(status) { + if (status) { return 🟢; } + else if (!status) { return 🔴; } + else { return ⚠️; } + } + + function parseDateTime(timestamp) { + return {timestamp}; + } + + return ( +
+

Utilities

+

All utilities from secure (admin only) api end point:

+ Add Utility + + + + + + + + + + + {utilities && utilities.map(utility => + + + + + + + )} + {!utilities && + + + + } + +
StatusUtilityLast Modified
{utilityStatus(utility.status)}{utility.name}{parseDateTime(utility.modified)} + Edit + +
+ +
+
+ ); +} + +export { List }; \ No newline at end of file From c21072069c61243786cb702308672b817f1a3523 Mon Sep 17 00:00:00 2001 From: jttps Date: Thu, 27 Aug 2020 19:51:40 +1000 Subject: [PATCH 4/6] Fixed add/edit utility feature --- src/_services/utilities.service.js | 9 --- src/admin/utilities/AddEdit.jsx | 117 +++++++---------------------- 2 files changed, 26 insertions(+), 100 deletions(-) diff --git a/src/_services/utilities.service.js b/src/_services/utilities.service.js index 6fb93dc..dd0f0d1 100644 --- a/src/_services/utilities.service.js +++ b/src/_services/utilities.service.js @@ -40,15 +40,6 @@ function create(params) { function update(id, params) { return fetchWrapper.put(`${baseUrl}/${id}`, params) - .then(utility => { - // update stored user if the logged in user updated their own record - if (utility.id === utilitySubject.value.id) { - // publish updated user to subscribers - utility = { ...utilitySubject.value, ...utility }; - utilitySubject.next(utility); - } - return utility; - }); } // prefixed with underscore because 'delete' is a reserved word in javascript diff --git a/src/admin/utilities/AddEdit.jsx b/src/admin/utilities/AddEdit.jsx index 8bd71b3..009d8b5 100644 --- a/src/admin/utilities/AddEdit.jsx +++ b/src/admin/utilities/AddEdit.jsx @@ -3,57 +3,37 @@ import { Link } from 'react-router-dom'; import { Formik, Field, Form, ErrorMessage } from 'formik'; import * as Yup from 'yup'; -import { accountService, alertService } from '@/_services'; +import { utilitiesService, alertService } from '@/_services'; function AddEdit({ history, match }) { const { id } = match.params; const isAddMode = !id; const initialValues = { - title: '', - firstName: '', - lastName: '', - email: '', - role: '', - password: '', - confirmPassword: '' + name: '', + status: '' }; const validationSchema = Yup.object().shape({ - title: Yup.string() - .required('Title is required'), - firstName: Yup.string() - .required('First Name is required'), - lastName: Yup.string() - .required('Last Name is required'), - email: Yup.string() - .email('Email is invalid') - .required('Email is required'), - role: Yup.string() - .required('Role is required'), - password: Yup.string() - .concat(isAddMode ? Yup.string().required('Password is required') : null) - .min(6, 'Password must be at least 6 characters'), - confirmPassword: Yup.string() - .when('password', (password, schema) => { - if (password) return schema.required('Confirm Password is required'); - }) - .oneOf([Yup.ref('password')], 'Passwords must match') + name: Yup.string() + .required('Utility name is required'), + status: Yup.boolean() + .required('Utility status is required') }); function onSubmit(fields, { setStatus, setSubmitting }) { setStatus(); if (isAddMode) { - createUser(fields, setSubmitting); + createUtility(fields, setSubmitting); } else { - updateUser(id, fields, setSubmitting); + updateUtility(id, fields, setSubmitting); } } - function createUser(fields, setSubmitting) { - accountService.create(fields) + function createUtility(fields, setSubmitting) { + utilitiesService.create(fields) .then(() => { - alertService.success('User added successfully', { keepAfterRouteChange: true }); + alertService.success('Utility added successfully', { keepAfterRouteChange: true }); history.push('.'); }) .catch(error => { @@ -62,8 +42,8 @@ function AddEdit({ history, match }) { }); } - function updateUser(id, fields, setSubmitting) { - accountService.update(id, fields) + function updateUtility(id, fields, setSubmitting) { + utilitiesService.update(id, fields) .then(() => { alertService.success('Update successful', { keepAfterRouteChange: true }); history.push('..'); @@ -79,78 +59,33 @@ function AddEdit({ history, match }) { {({ errors, touched, isSubmitting, setFieldValue }) => { useEffect(() => { if (!isAddMode) { - // get user and set form fields - accountService.getById(id).then(user => { - const fields = ['title', 'firstName', 'lastName', 'email', 'role']; - fields.forEach(field => setFieldValue(field, user[field], false)); + // get utility and set form fields + utilitiesService.getById(id).then(utility => { + const fields = ['name', 'status']; + fields.forEach(field => setFieldValue(field, utility[field], false)); }); } }, []); return (
-

{isAddMode ? 'Add User' : 'Edit User'}

+

{isAddMode ? 'Add Utility' : 'Edit Utility'}

-
- - - - - - - - - -
- - - + + +
- - - -
-
-
-
- - - -
-
- - - - - - - -
-
- {!isAddMode && -
-

Change Password

-

Leave blank to keep the same password

-
- } -
-
- - - -
-
- - - + + +
Cancel
From 1e46c9ec61defd26868c3072950d652af6fd5869 Mon Sep 17 00:00:00 2001 From: jttps Date: Thu, 27 Aug 2020 20:03:10 +1000 Subject: [PATCH 5/6] Added utilities navlink to admin navbar --- src/_components/Nav.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_components/Nav.jsx b/src/_components/Nav.jsx index f709a73..6e55e6e 100644 --- a/src/_components/Nav.jsx +++ b/src/_components/Nav.jsx @@ -39,6 +39,7 @@ function AdminNav({ match }) { ); From 7a7e30fbe87a35cef0b5f281caee475a4dee9908 Mon Sep 17 00:00:00 2001 From: jttps Date: Sat, 29 Aug 2020 23:20:55 +1000 Subject: [PATCH 6/6] Fixed enable/disable utilities front-end --- src/_services/utilities.service.js | 8 +++--- src/admin/utilities/AddEdit.jsx | 9 ++++--- src/admin/utilities/List.jsx | 42 +++++++++++++++++++++++++----- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/_services/utilities.service.js b/src/_services/utilities.service.js index dd0f0d1..6444d7a 100644 --- a/src/_services/utilities.service.js +++ b/src/_services/utilities.service.js @@ -18,12 +18,12 @@ export const utilitiesService = { get utilityValue () { return utilitySubject.value } }; -function enable() { - return fetchWrapper.get(`${baseUrl}/enable`); +function enable(id) { + return fetchWrapper.post(`${baseUrl}/enable/${id}`); } -function disable() { - return fetchWrapper.get(`${baseUrl}/disable`); +function disable(id) { + return fetchWrapper.post(`${baseUrl}/disable/${id}`); } function getAll() { diff --git a/src/admin/utilities/AddEdit.jsx b/src/admin/utilities/AddEdit.jsx index 009d8b5..bae1dfd 100644 --- a/src/admin/utilities/AddEdit.jsx +++ b/src/admin/utilities/AddEdit.jsx @@ -11,7 +11,7 @@ function AddEdit({ history, match }) { const initialValues = { name: '', - status: '' + status: 'true' }; const validationSchema = Yup.object().shape({ @@ -76,9 +76,12 @@ function AddEdit({ history, match }) { -
+
- + + + +
diff --git a/src/admin/utilities/List.jsx b/src/admin/utilities/List.jsx index 2ae977d..1496ad1 100644 --- a/src/admin/utilities/List.jsx +++ b/src/admin/utilities/List.jsx @@ -12,6 +12,18 @@ function List({ match }) { utilitiesService.getAll().then(x => setUtilities(x)); }, []); + function enableUtility(id) { + utilitiesService.enable(id).then(() => { + utilitiesService.getAll().then(x => setUtilities(x)); + }); + } + + function disableUtility(id) { + utilitiesService.disable(id).then(() => { + utilitiesService.getAll().then(x => setUtilities(x)); + }); + } + function deleteUtility(id) { setUtilities(utilities.map(x => { if (x.id === id) { x.isDeleting = true; } @@ -27,10 +39,23 @@ function List({ match }) { else if (!status) { return 🔴; } else { return ⚠️; } } + + function toggleUtility(id, status) { + if (status) { + disableUtility(id) + } else if (!status) { + enableUtility(id) + } + } + + function defaultChecked(status) { + if (status) { return "checked"; } + else { return null; } + } function parseDateTime(timestamp) { return {timestamp}; - } + } return (
@@ -49,12 +74,15 @@ function List({ match }) { {utilities && utilities.map(utility => - {utilityStatus(utility.status)} - {utility.name} - {parseDateTime(utility.modified)} - - Edit -