From cfb60c18fd55a151d5ea0bf85316523b6cbcbdc9 Mon Sep 17 00:00:00 2001 From: Vivek Singh Date: Thu, 23 May 2024 14:23:40 +0530 Subject: [PATCH] #1214 - use phone number library for number validation. user can enter phone number in multiple formats as used in real world - hence removed message to enter in 10 digits etc. --- .editorconfig | 3 - package.json | 3 +- src/adminApp/Account.js | 18 +- src/adminApp/AccountOrgAdminUser.js | 25 +-- src/adminApp/DeploymentManager.js | 23 +-- src/adminApp/OrgManager.js | 40 +--- src/adminApp/UserHelper.js | 41 ++-- src/adminApp/user.js | 296 ++++++---------------------- yarn.lock | 13 +- 9 files changed, 113 insertions(+), 349 deletions(-) diff --git a/.editorconfig b/.editorconfig index e9ceba47a..f6d3af7da 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,6 +7,3 @@ max_line_length = 140 [Makefile] indent_style = tab - -[*.js] -spaces_around_brackets = both diff --git a/package.json b/package.json index aec1a88b3..cafaffb3d 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,8 @@ "redux-saga": "0.16.2", "redux-thunk": "^2.3.0", "rules-config": "github:avniproject/rules-config#fe552da405368bfd138e2f38e605c1d307e3ebe4", - "uuid": "^3.3.2" + "uuid": "^3.3.2", + "libphonenumber-js": "^1.11.1" }, "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", diff --git a/src/adminApp/Account.js b/src/adminApp/Account.js index 7feca0956..a641a7ad7 100644 --- a/src/adminApp/Account.js +++ b/src/adminApp/Account.js @@ -1,17 +1,5 @@ import React from "react"; -import { - Create, - Datagrid, - Edit, - List, - Show, - SimpleForm, - SimpleShowLayout, - TextField, - TextInput, - Toolbar, - SaveButton -} from "react-admin"; +import { Create, Datagrid, Edit, List, Show, SimpleForm, SimpleShowLayout, TextField, TextInput, Toolbar, SaveButton } from "react-admin"; import { Title } from "./components/Title"; //To remove delete button from the toolbar @@ -25,6 +13,7 @@ export const AccountList = props => ( + ); @@ -33,6 +22,7 @@ export const AccountDetails = props => ( } {...props}> + ); @@ -41,6 +31,7 @@ export const AccountCreate = props => ( + ); @@ -49,6 +40,7 @@ export const AccountEdit = props => ( } {...props}> } redirect="list"> + ); diff --git a/src/adminApp/AccountOrgAdminUser.js b/src/adminApp/AccountOrgAdminUser.js index 670c1cdd5..dd79756bd 100644 --- a/src/adminApp/AccountOrgAdminUser.js +++ b/src/adminApp/AccountOrgAdminUser.js @@ -28,27 +28,25 @@ import EnableDisableButton from "./components/EnableDisableButton"; import { CustomToolbar, formatRoles, + getPhoneValidator, isRequired, - mobileNumberFormatter, - mobileNumberParser, PasswordTextField, UserFilter, UserTitle, - validateEmail, - validatePhone + validateEmail } from "./UserHelper"; import { TitleChip } from "./components/TitleChip"; import OrganisationService from "../common/service/OrganisationService"; -export const AccountOrgAdminUserCreate = ({ user, ...props }) => ( +export const AccountOrgAdminUserCreate = ({ user, region, ...props }) => ( - + ); -export const AccountOrgAdminUserEdit = ({ user, ...props }) => ( +export const AccountOrgAdminUserEdit = ({ user, region, ...props }) => ( } undoable={false} filter={{ searchURI: "orgAdmin" }}> - + ); @@ -99,7 +97,7 @@ export const AccountOrgAdminUserDetail = ({ user, ...props }) => ( ); -const UserForm = ({ edit, user, ...props }) => { +const UserForm = ({ edit, user, region, ...props }) => { const [nameSuffix, setNameSuffix] = useState(""); const getOrgData = id => id && OrganisationService.getOrganisation(id).then(data => setNameSuffix(data.usernameSuffix)); @@ -155,14 +153,7 @@ const UserForm = ({ edit, user, ...props }) => { {!edit && } - + ); }; diff --git a/src/adminApp/DeploymentManager.js b/src/adminApp/DeploymentManager.js index 37e792d83..ef1608e86 100644 --- a/src/adminApp/DeploymentManager.js +++ b/src/adminApp/DeploymentManager.js @@ -16,18 +16,8 @@ import { AccountOrgAdminUserEdit, AccountOrgAdminUserList } from "./AccountOrgAdminUser"; -import { - OrganisationCreate, - OrganisationDetails, - OrganisationEdit, - OrganisationList -} from "./Organisation"; -import { - organisationGroupCreate, - organisationGroupEdit, - OrganisationGroupList, - OrganisationGroupShow -} from "./OrganisationGroup"; +import { OrganisationCreate, OrganisationDetails, OrganisationEdit, OrganisationList } from "./Organisation"; +import { organisationGroupCreate, organisationGroupEdit, OrganisationGroupList, OrganisationGroupShow } from "./OrganisationGroup"; class DeploymentManager extends Component { static childContextTypes = { @@ -39,7 +29,7 @@ class DeploymentManager extends Component { } render() { - const { user } = this.props; + const { user, userInfo } = this.props; return ( ({ user: state.app.authSession, + userInfo: state.app.userInfo, organisations: state.app.organisations }); diff --git a/src/adminApp/OrgManager.js b/src/adminApp/OrgManager.js index a92b87b89..794e311f7 100644 --- a/src/adminApp/OrgManager.js +++ b/src/adminApp/OrgManager.js @@ -8,19 +8,9 @@ import { authProvider, LogoutButton } from "./react-admin-config"; import { adminHistory, store } from "../common/store"; import { UserCreate, UserDetail, UserEdit, UserList } from "./user"; import { CatchmentCreate, CatchmentDetail, CatchmentEdit, CatchmentList } from "./catchment"; -import { - LocationTypeCreate, - LocationTypeDetail, - LocationTypeEdit, - LocationTypeList -} from "./addressLevelType"; +import { LocationTypeCreate, LocationTypeDetail, LocationTypeEdit, LocationTypeList } from "./addressLevelType"; import { LocationCreate, LocationDetail, LocationEdit, LocationList } from "./locations"; -import { - IdentifierSourceCreate, - IdentifierSourceDetail, - IdentifierSourceEdit, - IdentifierSourceList -} from "./IdentifierSource"; +import { IdentifierSourceCreate, IdentifierSourceDetail, IdentifierSourceEdit, IdentifierSourceList } from "./IdentifierSource"; import { IdentifierUserAssignmentCreate, IdentifierUserAssignmentDetail, @@ -120,15 +110,9 @@ class OrgManager extends Component { ) : (
@@ -158,12 +142,8 @@ class OrgManager extends Component { options={{ label: "Identifier User Assignment" }} list={IdentifierUserAssignmentList} show={IdentifierUserAssignmentDetail} - create={ - hasPrivilege(userInfo, EditIdentifierUserAssignment) && IdentifierUserAssignmentCreate - } - edit={ - hasPrivilege(userInfo, EditIdentifierUserAssignment) && IdentifierUserAssignmentEdit - } + create={hasPrivilege(userInfo, EditIdentifierUserAssignment) && IdentifierUserAssignmentCreate} + edit={hasPrivilege(userInfo, EditIdentifierUserAssignment) && IdentifierUserAssignmentEdit} /> {hasPrivilege(userInfo, PhoneVerification) ? ( - + ) : (
)} diff --git a/src/adminApp/UserHelper.js b/src/adminApp/UserHelper.js index 2783360ca..32bffc41f 100644 --- a/src/adminApp/UserHelper.js +++ b/src/adminApp/UserHelper.js @@ -1,16 +1,7 @@ import React from "react"; -import { isEmpty, isEqual, isNil } from "lodash"; -import { phoneCountryPrefix } from "../common/constants"; -import { - email, - Filter, - regex, - required, - SaveButton, - TextInput, - Toolbar, - minLength -} from "react-admin"; +import { isEmpty, isEqual } from "lodash"; +import { email, Filter, minLength, required, SaveButton, TextInput, Toolbar } from "react-admin"; +import { isValidPhoneNumber } from "libphonenumber-js"; export const UserTitle = ({ record, titlePrefix }) => { return ( @@ -57,21 +48,21 @@ export const PasswordTextField = props => ( ); -export const mobileNumberFormatter = (v = "") => - isNil(v) ? v : v.substring(phoneCountryPrefix.length); -export const mobileNumberParser = v => - v.startsWith(phoneCountryPrefix) ? v : phoneCountryPrefix.concat(v); - export const isRequired = required("This field is required"); export const validateEmail = [isRequired, email("Please enter a valid email address")]; -export const validatePhone = [ - isRequired, - regex(/[0-9]{12}/, "Enter a 10 digit number (eg. 9820324567)") -]; -export const validatePassword = [ - isRequired, - minLength(8, "Password too small, enter at least 8 characters.") -]; + +const getValidatePhoneValidator = function(region) { + return value => { + const isValid = isValidPhoneNumber(value, region); + return isValid ? undefined : "Invalid phone number"; + }; +}; + +export const getPhoneValidator = function(region) { + return [isRequired, getValidatePhoneValidator(region)]; +}; + +export const validatePassword = [isRequired, minLength(8, "Password too small, enter at least 8 characters.")]; export const validatePasswords = ({ password, confirmPassword }) => { const errors = {}; if (!isEqual(password, confirmPassword)) { diff --git a/src/adminApp/user.js b/src/adminApp/user.js index 477ee1e1c..e350bf2cf 100644 --- a/src/adminApp/user.js +++ b/src/adminApp/user.js @@ -1,10 +1,10 @@ -import _, { filter, get, isEmpty, isFinite, isNil, map, some, startCase, sortBy } from "lodash"; +import _, { filter, get, isEmpty, isFinite, isNil, map, some, sortBy, startCase } from "lodash"; import React, { cloneElement, Fragment, useContext, useEffect, useState } from "react"; import { ArrayField, ArrayInput, - Create, ChipField, + Create, Datagrid, DisabledInput, Edit, @@ -37,15 +37,13 @@ import EnableDisableButton from "./components/EnableDisableButton"; import http, { httpClient } from "common/utils/httpClient"; import { CustomToolbar, + getPhoneValidator, isRequired, - mobileNumberFormatter, - mobileNumberParser, UserFilter, UserTitle, validateEmail, validatePassword, - validatePasswords, - validatePhone + validatePasswords } from "./UserHelper"; import { DocumentationContainer } from "../common/components/DocumentationContainer"; import { ToolTipContainer } from "../common/components/ToolTipContainer"; @@ -68,15 +66,15 @@ export const UserCreate = ({ user, organisation, userInfo, ...props }) => ( - + ); -export const UserEdit = ({ ...props }) => ( +export const UserEdit = ({ organisation, ...props }) => ( } undoable={false}> - + ); @@ -85,9 +83,7 @@ const UserGroupsDisplay = ({ record, style }) => ( {_.isArrayLike(record.userGroups) && record.userGroups .filter(ug => ug && !ug.voided) - .map(userGroup => ( - - ))} + .map(userGroup => )}
); @@ -110,13 +106,7 @@ export const UserList = ({ ...props }) => { - + @@ -124,13 +114,7 @@ export const UserList = ({ ...props }) => { - user.voided === true - ? "Deleted" - : user.disabledInCognito === true - ? "Disabled" - : "Active" - } + render={user => (user.voided === true ? "Deleted" : user.disabledInCognito === true ? "Disabled" : "Active")} /> @@ -145,12 +129,7 @@ const CustomShowActions = ({ hasEditUserPrivilege, basePath, data, resource }) = - + )} @@ -169,9 +148,7 @@ const formatLang = lang => const SubjectTypeSyncAttributeShow = ({ subjectType, syncConceptValueMap, ...props }) => (
- {`Sync settings for Subject Type: ${ - subjectType.name - }`} + {`Sync settings for Subject Type: ${subjectType.name}`} {subjectType.syncAttribute1 && ( ); -const ConceptSyncAttributeShow = ({ - subjectType, - syncConceptValueMap, - syncAttributeName, - ...props -}) => { +const ConceptSyncAttributeShow = ({ subjectType, syncConceptValueMap, syncAttributeName, ...props }) => { const syncSettings = get(props.record, ["syncSettings", subjectType.name], {}); const conceptUUID = get(syncSettings, [syncAttributeName]); if (isEmpty(conceptUUID)) return null; return (
- - {startCase(syncAttributeName)} - + {startCase(syncAttributeName)} {map(get(syncSettings, `${syncAttributeName}Values`, []), value => ( - + ))}
); @@ -222,23 +188,13 @@ export const UserDetail = ({ user, hasEditUserPrivilege, ...props }) => { fetchSyncAttributeData(setSyncAttributesData); return ( - } - actions={} - {...props} - > + } actions={} {...props}> - + @@ -248,10 +204,7 @@ export const UserDetail = ({ user, hasEditUserPrivilege, ...props }) => { - formatOperatingScope(user.operatingIndividualScope)} - /> + formatOperatingScope(user.operatingIndividualScope)} /> {map(syncAttributesData.subjectTypes, st => ( { syncConceptValueMap={syncAttributesData.syncConceptValueMap} /> ))} - (!isNil(user.settings) ? formatLang(user.settings.locale) : "")} - /> - (!isNil(user.settings) ? user.settings.datePickerMode : "Calendar")} - /> - (!isNil(user.settings) ? user.settings.timePickerMode : "Clock")} - /> + (!isNil(user.settings) ? formatLang(user.settings.locale) : "")} /> + (!isNil(user.settings) ? user.settings.datePickerMode : "Calendar")} /> + (!isNil(user.settings) ? user.settings.timePickerMode : "Clock")} /> - !isNil(user.settings) ? (user.settings.trackLocation ? "True" : "False") : "" - } + render={user => (!isNil(user.settings) ? (user.settings.trackLocation ? "True" : "False") : "")} /> - !isNil(user.settings) ? (user.settings.showBeneficiaryMode ? "True" : "False") : "" - } + render={user => (!isNil(user.settings) ? (user.settings.showBeneficiaryMode ? "True" : "False") : "")} /> - !isNil(user.settings) ? (user.settings.disableAutoRefresh ? "True" : "False") : "" - } + render={user => (!isNil(user.settings) ? (user.settings.disableAutoRefresh ? "True" : "False") : "")} /> - !isNil(user.settings) ? (user.settings.disableAutoSync ? "True" : "False") : "" - } + render={user => (!isNil(user.settings) ? (user.settings.disableAutoSync ? "True" : "False") : "")} /> - !isNil(user.settings) ? (user.settings.registerEnrol ? "True" : "False") : "" - } + render={user => (!isNil(user.settings) ? (user.settings.registerEnrol ? "True" : "False") : "")} /> - !isNil(user.settings) ? (user.settings.enableCallMasking ? "True" : "False") : "" - } + render={user => (!isNil(user.settings) ? (user.settings.enableCallMasking ? "True" : "False") : "")} /> createdAudit(audit)} /> @@ -328,23 +260,9 @@ This might take time depending on the data.`; const SubjectTypeSyncAttributes = ({ subjectType, ...props }) => (
- {`Sync settings for Subject Type: ${ - subjectType.name - }`} - {subjectType.syncAttribute1 && ( - - )} - {subjectType.syncAttribute2 && ( - - )} + {`Sync settings for Subject Type: ${subjectType.name}`} + {subjectType.syncAttribute1 && } + {subjectType.syncAttribute2 && }
); @@ -352,26 +270,16 @@ const ConceptSyncAttribute = ({ subjectType, syncAttributeName, edit, ...props } return ( {({ formData, dispatch }) => { - const syncAttributeConceptUUID = get( - formData, - `syncSettings.${[subjectType.name]}.${syncAttributeName}` - ); + const syncAttributeConceptUUID = get(formData, `syncSettings.${[subjectType.name]}.${syncAttributeName}`); const syncAttributeConcept = subjectType[syncAttributeName]; - const selectedValue = get( - formData, - `syncSettings.${[subjectType.name]}.${syncAttributeName}` - ); + const selectedValue = get(formData, `syncSettings.${[subjectType.name]}.${syncAttributeName}`); const defaultValue = selectedValue ? { defaultValue: selectedValue } : {}; const [answerConcepts, setAnswerConcepts] = useState([]); - const syncAttributeValuesFieldName = `syncSettings.${[ - subjectType.name - ]}.${syncAttributeName}Values`; + const syncAttributeValuesFieldName = `syncSettings.${[subjectType.name]}.${syncAttributeName}Values`; const selectedSyncAttributeValueIds = get(formData, syncAttributeValuesFieldName); - const selectedAnswerConcepts = filter(answerConcepts, x => - some(selectedSyncAttributeValueIds, y => x.id === y) - ); + const selectedAnswerConcepts = filter(answerConcepts, x => some(selectedSyncAttributeValueIds, y => x.id === y)); useEffect(() => { if (isEmpty(syncAttributeConceptUUID)) setAnswerConcepts([]); @@ -409,31 +317,16 @@ const ConceptSyncAttribute = ({ subjectType, syncAttributeName, edit, ...props } options={answerConcepts.map(ReactSelectHelper.toReactSelectItem)} style={{ width: "auto" }} onChange={event => { - const selectedValues = ReactSelectHelper.getCurrentValues(event).map( - x => x.id - ); - dispatch( - change(REDUX_FORM_NAME, syncAttributeValuesFieldName, selectedValues) - ); + const selectedValues = ReactSelectHelper.getCurrentValues(event).map(x => x.id); + dispatch(change(REDUX_FORM_NAME, syncAttributeValuesFieldName, selectedValues)); }} /> ) : ( -
- {"Values to sync"} -
- +
{"Values to sync"}
+ - +
@@ -454,14 +347,8 @@ const getSyncConceptValueMap = async sortedSubjectTypes => { const syncConceptValueMap = new Map(); const codedConceptUUIDSet = new Set(); sortedSubjectTypes.forEach(subject => { - const syncAttribute1UUID = - subject.syncAttribute1 && - subject.syncAttribute1.dataType === "Coded" && - subject.syncAttribute1.id; - const syncAttribute2UUID = - subject.syncAttribute2 && - subject.syncAttribute2.dataType === "Coded" && - subject.syncAttribute2.id; + const syncAttribute1UUID = subject.syncAttribute1 && subject.syncAttribute1.dataType === "Coded" && subject.syncAttribute1.id; + const syncAttribute2UUID = subject.syncAttribute2 && subject.syncAttribute2.dataType === "Coded" && subject.syncAttribute2.id; syncAttribute1UUID && codedConceptUUIDSet.add(syncAttribute1UUID); syncAttribute2UUID && codedConceptUUIDSet.add(syncAttribute2UUID); }); @@ -479,11 +366,7 @@ function fetchSyncAttributeData(setSyncAttributesData) { useEffect(() => { let isMounted = true; http.get("/subjectType/syncAttributesData").then(res => { - const { - subjectTypes, - anySubjectTypeDirectlyAssignable, - anySubjectTypeSyncByLocation - } = res.data; + const { subjectTypes, anySubjectTypeDirectlyAssignable, anySubjectTypeSyncByLocation } = res.data; const sortedSubjectTypes = sortBy(subjectTypes, "id"); getSyncConceptValueMap(sortedSubjectTypes).then(syncConceptValueMap => { @@ -503,20 +386,16 @@ function fetchSyncAttributeData(setSyncAttributesData) { }, []); } -const UserForm = ({ edit, nameSuffix, ...props }) => { +const UserForm = ({ edit, nameSuffix, organisation, ...props }) => { const [languages, setLanguages] = useState([]); const [syncAttributesData, setSyncAttributesData] = useState(initialSyncAttributes); - const isSyncSettingsRequired = - syncAttributesData.subjectTypes.length > 0 || - syncAttributesData.isAnySubjectTypeDirectlyAssignable; + const isSyncSettingsRequired = syncAttributesData.subjectTypes.length > 0 || syncAttributesData.isAnySubjectTypeDirectlyAssignable; useEffect(() => { http.get("/organisationConfig").then(res => { const organisationLocales = isEmpty(res.data._embedded.organisationConfig) ? [localeChoices[0]] - : filter(localeChoices, l => - res.data._embedded.organisationConfig[0].settings.languages.includes(l.id) - ); + : filter(localeChoices, l => res.data._embedded.organisationConfig[0].settings.languages.includes(l.id)); setLanguages(organisationLocales); }); }, []); @@ -529,12 +408,7 @@ const UserForm = ({ edit, nameSuffix, ...props }) => { save }); return ( - } - {...sanitizeProps(props)} - redirect="show" - validate={validatePasswords} - > + } {...sanitizeProps(props)} redirect="show" validate={validatePasswords}> {edit ? ( ) : ( @@ -547,10 +421,7 @@ const UserForm = ({ edit, nameSuffix, ...props }) => { source="ignored" validate={isRequired} label={"Login ID (username)"} - onChange={(e, newVal) => - !isEmpty(newVal) && - dispatch(change(REDUX_FORM_NAME, "username", newVal + "@" + nameSuffix)) - } + onChange={(e, newVal) => !isEmpty(newVal) && dispatch(change(REDUX_FORM_NAME, "username", newVal + "@" + nameSuffix))} {...rest} toolTipKey={"ADMIN_USER_USER_NAME"} > @@ -595,28 +466,10 @@ const UserForm = ({ edit, nameSuffix, ...props }) => { }}
)} - - + + - + {({ formData, dispatch, ...rest }) => ( @@ -631,18 +484,11 @@ const UserForm = ({ edit, nameSuffix, ...props }) => { reference="catchment" label="Which catchment?" filterToQuery={searchText => ({ name: searchText })} - validate={ - syncAttributesData.isAnySubjectTypeSyncByLocation && - required("Please select a catchment") - } + validate={syncAttributesData.isAnySubjectTypeSyncByLocation && required("Please select a catchment")} onChange={(e, newVal) => { if (edit) alert(catchmentChangeMessage); dispatch( - change( - REDUX_FORM_NAME, - "operatingIndividualScope", - isFinite(newVal) ? operatingScopes.CATCHMENT : operatingScopes.NONE - ) + change(REDUX_FORM_NAME, "operatingIndividualScope", isFinite(newVal) ? operatingScopes.CATCHMENT : operatingScopes.NONE) ); }} {...rest} @@ -659,26 +505,12 @@ const UserForm = ({ edit, nameSuffix, ...props }) => { User Groups - - + + - + @@ -687,11 +519,7 @@ const UserForm = ({ edit, nameSuffix, ...props }) => { - + { label="Disable auto sync" toolTipKey={"ADMIN_USER_SETTINGS_DISABLE_AUTO_SYNC"} /> - + - +