Version 0.9.71

This commit is contained in:
Charles
2021-10-19 11:14:31 -04:00
parent e7e8b719d9
commit 73ac76f2d0
29 changed files with 2026 additions and 146 deletions

82
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-libs",
"version": "0.9.51",
"version": "0.9.71",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-libs",
"version": "0.9.51",
"version": "0.9.71",
"license": "BSD-3-Clause",
"dependencies": {
"@coreui/coreui": "^3.4.0",
@@ -14,9 +14,11 @@
"@coreui/icons-react": "^1.1.0",
"@coreui/react": "^3.4.6",
"@coreui/react-chartjs": "^1.1.0",
"libphonenumber-js": "^1.9.37",
"lodash": "^4.17.21",
"react-flow-renderer": "^9.6.6",
"react-paginate": "^7.1.3",
"react-phone-input-2": "^2.14.0",
"react-router-dom": "^5.2.0",
"react-select": "^4.3.1",
"react-tooltip": "^4.2.21",
@@ -8278,6 +8280,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/libphonenumber-js": {
"version": "1.9.37",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.37.tgz",
"integrity": "sha512-RnUR4XwiVhMLnT7uFSdnmLeprspquuDtaShAgKTA+g/ms9/S4hQU3/QpFdh3iXPHtxD52QscXLm2W2+QBmvYAg=="
},
"node_modules/lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
@@ -8518,8 +8525,12 @@
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
"dev": true
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
@@ -8527,6 +8538,16 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"node_modules/lodash.reduce": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
"integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs="
},
"node_modules/lodash.startswith": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/lodash.startswith/-/lodash.startswith-4.2.1.tgz",
"integrity": "sha1-xZjErc4YiiflMUVzHNxsDnF3YAw="
},
"node_modules/lodash.truncate": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
@@ -10482,6 +10503,23 @@
"react": "^16.0.0 || ^17.0.0"
}
},
"node_modules/react-phone-input-2": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/react-phone-input-2/-/react-phone-input-2-2.14.0.tgz",
"integrity": "sha512-gOY3jUpwO7ulryXPEdqzH7L6DPqI9RQxKfBxZbgqAwXyALGsmwLWFyi2RQwXlBLWN/EPPT4Nv6I9TESVY2YBcg==",
"dependencies": {
"classnames": "^2.2.6",
"lodash.debounce": "^4.0.8",
"lodash.memoize": "^4.1.2",
"lodash.reduce": "^4.6.0",
"lodash.startswith": "^4.2.1",
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": "^16.12.0 || ^17.0.0",
"react-dom": "^16.12.0 || ^17.0.0"
}
},
"node_modules/react-redux": {
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz",
@@ -20369,6 +20407,11 @@
"type-check": "~0.4.0"
}
},
"libphonenumber-js": {
"version": "1.9.37",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.37.tgz",
"integrity": "sha512-RnUR4XwiVhMLnT7uFSdnmLeprspquuDtaShAgKTA+g/ms9/S4hQU3/QpFdh3iXPHtxD52QscXLm2W2+QBmvYAg=="
},
"lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
@@ -20557,8 +20600,12 @@
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
"dev": true
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
},
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4="
},
"lodash.merge": {
"version": "4.6.2",
@@ -20566,6 +20613,16 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"lodash.reduce": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
"integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs="
},
"lodash.startswith": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/lodash.startswith/-/lodash.startswith-4.2.1.tgz",
"integrity": "sha1-xZjErc4YiiflMUVzHNxsDnF3YAw="
},
"lodash.truncate": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
@@ -22049,6 +22106,19 @@
"prop-types": "^15.6.1"
}
},
"react-phone-input-2": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/react-phone-input-2/-/react-phone-input-2-2.14.0.tgz",
"integrity": "sha512-gOY3jUpwO7ulryXPEdqzH7L6DPqI9RQxKfBxZbgqAwXyALGsmwLWFyi2RQwXlBLWN/EPPT4Nv6I9TESVY2YBcg==",
"requires": {
"classnames": "^2.2.6",
"lodash.debounce": "^4.0.8",
"lodash.memoize": "^4.1.2",
"lodash.reduce": "^4.6.0",
"lodash.startswith": "^4.2.1",
"prop-types": "^15.7.2"
}
},
"react-redux": {
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-libs",
"version": "0.9.51",
"version": "0.9.71",
"main": "dist/index.js",
"source": "src/index.js",
"engines": {
@@ -9,20 +9,22 @@
"files": [
"dist"
],
"license" : "BSD-3-Clause",
"license": "BSD-3-Clause",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",
"@coreui/icons-react": "^1.1.0",
"@coreui/react": "^3.4.6",
"@coreui/react-chartjs": "^1.1.0",
"react-tooltip": "^4.2.21",
"libphonenumber-js": "^1.9.37",
"lodash": "^4.17.21",
"react-flow-renderer": "^9.6.6",
"react-paginate": "^7.1.3",
"react-phone-input-2": "^2.14.0",
"react-router-dom": "^5.2.0",
"react-select": "^4.3.1",
"uuid": "^8.3.2",
"lodash": "^4.17.21"
"react-tooltip": "^4.2.21",
"uuid": "^8.3.2"
},
"peerDependencies": {
"prop-types": "^15.7.2",

View File

@@ -0,0 +1,353 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Select from 'react-select';
import CreatableSelect from 'react-select/creatable';
import {
CForm,
CInput,
CLabel,
CCol,
CFormGroup,
CInvalidFeedback,
CFormText,
CRow,
CDataTable,
CLink,
CPopover,
CButton,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilPlus } from '@coreui/icons';
import FormattedDate from '../FormattedDate';
const AddContactForm = ({ t, disable, fields, updateField, updateFieldWithKey, entities }) => {
const [filter, setFilter] = useState('');
const [selectedEntity, setSelectedEntity] = useState('');
const onPhonesChange = (v) => updateFieldWithKey('phones', { value: v.map((obj) => obj.value) });
const onMobilesChange = (v) =>
updateFieldWithKey('mobiles', { value: v.map((obj) => obj.value) });
const NoOptionsMessage = () => (
<h6 className="text-center pt-2">{t('common.type_for_options')}</h6>
);
const columns = [
{ key: 'created', label: t('common.created'), _style: { width: '20%' }, filter: false },
{ key: 'name', label: t('user.name'), _style: { width: '25%' }, filter: false },
{ key: 'description', label: t('user.description'), _style: { width: '50%' } },
{ key: 'actions', label: '', _style: { width: '15%' }, filter: false },
];
const selectEntity = ({ id, name }) => {
updateFieldWithKey('entity', { value: id });
setSelectedEntity(name);
};
return (
<CForm>
<CRow>
<CLabel className="mb-5" sm="2" col htmlFor="type">
{t('contact.type')}
</CLabel>
<CCol sm="4">
<div style={{ width: '200px' }}>
<Select
id="type"
value={
fields.type.value !== ''
? { value: fields.type.value, label: fields.type.value }
: null
}
onChange={(v) => updateFieldWithKey('type', { value: v.value })}
options={[
{ label: 'SUBSCRIBER', value: 'SUBSCRIBER' },
{ label: 'USER', value: 'USER' },
{ label: 'INSTALLER', value: 'INSTALLER' },
{ label: 'CSR', value: 'CSR' },
{ label: 'MANAGER', value: 'MANAGER' },
{ label: 'BUSINESSOWNER', value: 'BUSINESSOWNER' },
{ label: 'TECHNICIAN', value: 'TECHNICIAN' },
{ label: 'CORPORATE', value: 'CORPORATE' },
]}
isDisabled={disable}
/>
</div>
<CFormText color={fields.type.error ? 'danger' : ''}>{t('common.required')}</CFormText>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="title">
{t('contact.user_title')}
</CLabel>
<CCol sm="4">
<CInput
id="title"
type="text"
required
value={fields.title.value}
onChange={updateField}
invalid={fields.title.error}
disabled={disable}
maxLength="50"
/>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="salutation">
{t('contact.salutation')}
</CLabel>
<CCol sm="4">
<div style={{ width: '120px' }}>
<Select
id="salutation"
value={{
value: fields.salutation.value,
label: fields.salutation.value === '' ? 'None' : fields.salutation.value,
}}
onChange={(v) => updateFieldWithKey('salutation', { value: v.value })}
options={[
{ label: 'None', value: '' },
{ label: 'Mr.', value: 'Mr.' },
{ label: 'Ms.', value: 'Ms.' },
{ label: 'Mx.', value: 'Mx.' },
{ label: 'Dr.', value: 'Dr.' },
]}
isDisabled={disable}
/>
</div>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="accessPIN">
{t('contact.access_pin')}
</CLabel>
<CCol sm="4">
<CInput
id="accessPIN"
type="text"
required
value={fields.accessPIN.value}
onChange={updateField}
invalid={fields.accessPIN.error}
disabled={disable}
maxLength="50"
/>
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="firstname">
{t('contact.first_name')}
</CLabel>
<CCol sm="4">
<CInput
id="firstname"
type="text"
required
value={fields.firstname.value}
onChange={updateField}
invalid={fields.firstname.error}
disabled={disable}
maxLength="50"
/>
<CFormText color={fields.firstname.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="lastname">
{t('contact.last_name')}
</CLabel>
<CCol sm="4">
<CInput
id="lastname"
type="text"
required
value={fields.lastname.value}
onChange={updateField}
invalid={fields.lastname.error}
disabled={disable}
maxLength="50"
/>
<CFormText color={fields.lastname.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="initials">
{t('contact.initials')}
</CLabel>
<CCol sm="4">
<CInput
id="initials"
type="text"
required
value={fields.initials.value}
onChange={updateField}
disabled={disable}
maxLength="50"
/>
</CCol>
<CLabel sm="2" col htmlFor="visual">
{t('contact.visual')}
</CLabel>
<CCol sm="4">
<CInput
id="visual"
type="text"
required
value={fields.visual.value}
onChange={updateField}
disabled={disable}
maxLength="50"
/>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="primaryEmail">
{t('contact.primary_email')}
</CLabel>
<CCol sm="4">
<CInput
id="primaryEmail"
type="text"
required
value={fields.primaryEmail.value}
onChange={updateField}
invalid={fields.primaryEmail.error}
disabled={disable}
maxLength="50"
/>
<CFormText color={fields.primaryEmail.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="secondaryEmail">
{t('contact.secondary_email')}
</CLabel>
<CCol sm="4">
<CInput
id="secondaryEmail"
type="text"
required
value={fields.secondaryEmail.value}
onChange={updateField}
invalid={fields.secondaryEmail.error}
disabled={disable}
maxLength="50"
/>
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="phones">
Landlines
</CLabel>
<CCol sm="4">
<CreatableSelect
isMulti
id="phones"
isDisabled={disable}
onChange={onPhonesChange}
components={{ NoOptionsMessage }}
options={[]}
value={fields.phones.value.map((opt) => ({ value: opt, label: opt }))}
placeholder={t('common.type_for_options')}
/>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="phones">
Mobiles
</CLabel>
<CCol sm="4">
<CreatableSelect
id="mobiles"
isMulti
isDisabled={disable}
onChange={onMobilesChange}
components={{ NoOptionsMessage }}
options={[]}
value={fields.mobiles.value.map((opt) => ({ value: opt, label: opt }))}
placeholder={t('common.type_for_options')}
/>
</CCol>
<CLabel sm="2" col htmlFor="description">
{t('user.description')}
</CLabel>
<CCol sm="4">
<CInput
id="description"
type="text"
required
value={fields.description.value}
onChange={updateField}
disabled={disable}
maxLength="50"
/>
</CCol>
<CLabel sm="2" col htmlFor="initialNote">
{t('user.note')}
</CLabel>
<CCol sm="4">
<CInput
id="initialNote"
type="text"
required
value={fields.initialNote.value}
onChange={updateField}
disabled={disable}
maxLength="50"
/>
</CCol>
</CRow>
<CFormGroup row className="pt-2 pb-1">
<CLabel sm="2" col htmlFor="title">
{t('entity.selected_entity')}
</CLabel>
<CCol sm="4" className="pt-2">
<h6>{fields.entity.value === '' ? t('entity.need_select_entity') : selectedEntity}</h6>
</CCol>
</CFormGroup>
<div className="overflow-auto border mb-1" style={{ height: '200px' }}>
<CInput
className="w-50 mb-2"
type="text"
placeholder="Search"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<CDataTable
items={entities}
fields={columns}
hover
tableFilterValue={filter}
border
scopedSlots={{
name: (item) => (
<td>
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/configuration/${item.id}`}
>
{item.name}
</CLink>
</td>
),
created: (item) => (
<td>
<FormattedDate date={item.created} />
</td>
),
actions: (item) => (
<td>
<CPopover content={t('entity.select_entity')}>
<CButton color="primary" variant="outline" onClick={() => selectEntity(item)}>
<CIcon content={cilPlus} />
</CButton>
</CPopover>
</td>
),
}}
/>
</div>
</CForm>
);
};
AddContactForm.propTypes = {
t: PropTypes.func.isRequired,
disable: PropTypes.bool.isRequired,
fields: PropTypes.instanceOf(Object).isRequired,
updateField: PropTypes.func.isRequired,
updateFieldWithKey: PropTypes.func.isRequired,
entities: PropTypes.instanceOf(Array).isRequired,
};
export default AddContactForm;

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import {
CFormGroup,
@@ -13,7 +13,7 @@ import {
CDataTable,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilMinus, cilSave, cilX } from '@coreui/icons';
import { cilMinus, cilPlus, cilSave, cilX } from '@coreui/icons';
import useToggle from '../../../hooks/useToggle';
const CustomMultiModal = ({
@@ -28,15 +28,17 @@ const CustomMultiModal = ({
secondCol,
length,
modalSize,
itemName,
noTable,
toggleAdd,
reset,
}) => {
const [show, toggle] = useToggle();
const getLabel = () => {
if (length === 0) return t('common.add_items');
if (length === 0) return `Manage ${itemName}`;
if (length === 1) return `${length} ${t('common.item')}`;
return `${length} ${t('common.items')}`;
return `Manage ${itemName} (${length})`;
};
const remove = (v) => {
@@ -55,6 +57,10 @@ const CustomMultiModal = ({
toggle();
};
useEffect(() => {
if (show && reset !== null) reset();
}, [show]);
return (
<CFormGroup row className="py-1">
<CLabel col sm={firstCol} htmlFor="name">
@@ -69,6 +75,17 @@ const CustomMultiModal = ({
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{label}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.add')}>
<CButton
color="primary"
hidden={toggleAdd === null}
variant="outline"
className="ml-2"
onClick={toggleAdd}
>
<CIcon content={cilPlus} />
</CButton>
</CPopover>
<CPopover content={t('common.save')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={closeAndSave}>
<CIcon content={cilSave} />
@@ -83,22 +100,29 @@ const CustomMultiModal = ({
</CModalHeader>
<CModalBody>
{children}
<CDataTable
addTableClasses="ignore-overflow"
items={value ?? []}
fields={columns}
hover
border
scopedSlots={{
remove: (item) => (
<td className="align-middle text-center">
<CButton color="primary" variant="outline" size="sm" onClick={() => remove(item)}>
<CIcon content={cilMinus} />
</CButton>
</td>
),
}}
/>
{!noTable ? (
<CDataTable
addTableClasses="ignore-overflow"
items={value ?? []}
fields={columns}
hover
border
scopedSlots={{
remove: (item) => (
<td className="align-middle text-center">
<CButton
color="primary"
variant="outline"
size="sm"
onClick={() => remove(item)}
>
<CIcon content={cilMinus} />
</CButton>
</td>
),
}}
/>
) : null}
</CModalBody>
</CModal>
</CFormGroup>
@@ -117,12 +141,19 @@ CustomMultiModal.propTypes = {
secondCol: PropTypes.string,
length: PropTypes.number.isRequired,
modalSize: PropTypes.string,
itemName: PropTypes.string.isRequired,
noTable: PropTypes.bool,
toggleAdd: PropTypes.func,
reset: PropTypes.func,
};
CustomMultiModal.defaultProps = {
firstCol: 6,
secondCol: 6,
modalSize: 'lg',
noTable: false,
toggleAdd: null,
reset: null,
};
export default CustomMultiModal;

View File

@@ -0,0 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CFormGroup, CCol, CLabel, CInvalidFeedback } from '@coreui/react';
const textToShow = (fieldValue, fileName) => {
if (fieldValue === '') return 'Not uploaded yet';
if (fileName === '') return 'Filename unavailable';
return `(using file: ${fileName})`;
};
const StringField = ({
fileName,
fieldValue,
label,
firstCol,
secondCol,
errorMessage,
extraButton,
}) => (
<CFormGroup row className="py-1">
<CLabel col sm={firstCol} htmlFor="name">
{label}
</CLabel>
<CCol className="align-middle" sm={secondCol}>
<div className="float-left pt-2">{textToShow(fieldValue, fileName)}</div>
<div className="float-left pl-3">{extraButton}</div>
<CInvalidFeedback>{errorMessage}</CInvalidFeedback>
</CCol>
</CFormGroup>
);
StringField.propTypes = {
fieldValue: PropTypes.string.isRequired,
fileName: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
firstCol: PropTypes.string,
secondCol: PropTypes.string,
errorMessage: PropTypes.string,
extraButton: PropTypes.node,
};
StringField.defaultProps = {
firstCol: 6,
secondCol: 6,
errorMessage: '',
extraButton: null,
};
export default StringField;

View File

@@ -27,6 +27,16 @@ const ConfigurationSelectField = ({
return null;
}
if (typeof field.options[0] === 'number') {
for (const opt of field.options) {
const value = opt;
if (value === field.value) {
return { value, label: opt };
}
}
return null;
}
if (options !== null) {
for (const opt of options) {
if (field.value === opt.value) return opt;
@@ -34,7 +44,10 @@ const ConfigurationSelectField = ({
}
for (const opt of field.options) {
if (field.value === opt.value) return opt;
if (field.value === opt.value) {
if (field.mergeOptions) return { value: opt.value, label: `${opt.label} (${opt.value})` };
return opt;
}
}
return null;
@@ -42,10 +55,22 @@ const ConfigurationSelectField = ({
const parseOptions = () => {
if (options !== null) return options;
if (typeof field.options[0] !== 'string') {
if (typeof field.options[0] !== 'string' && typeof field.options[0] !== 'number') {
if (field.mergeOptions)
return field.options.map((opt) => ({
value: opt.value,
label: `${opt.label} (${opt.value})`,
}));
return field.options;
}
if (typeof field.options[0] === 'number') {
return field.options.map((opt) => ({
value: opt,
label: `${opt}`,
}));
}
if (field.options[0].includes('(')) {
return field.options.map((opt) => ({
value: opt.split('(')[1].split(')')[0],
@@ -54,7 +79,7 @@ const ConfigurationSelectField = ({
}
return field.options.map((opt) => ({
value: opt,
label: `opt${field.unit ?? ''}`,
label: `${opt}${field.unit ?? ''}`,
}));
};
@@ -64,7 +89,7 @@ const ConfigurationSelectField = ({
{label}
</CLabel>
<CCol sm={secondCol}>
<div style={{ width: width ?? '' }}>
<div style={{ maxWidth: width ?? '' }}>
<Select
name="Subsystems"
options={parseOptions()}

View File

@@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { CFormGroup, CCol, CLabel, CInput, CInvalidFeedback } from '@coreui/react';
import _ from 'lodash';
@@ -14,6 +14,7 @@ const StringField = ({
disabled,
width,
placeholder,
extraButton,
}) => {
const [localValue, setLocalValue] = useState(field.value);
@@ -41,6 +42,36 @@ const StringField = ({
[localValue, debounceChange, updateField],
);
useEffect(() => {
if (localValue !== field.value) setLocalValue(field.value);
}, [field]);
if (extraButton !== null) {
return (
<CFormGroup row className="py-1">
<CLabel col sm={firstCol} htmlFor="name">
{label}
</CLabel>
<CCol sm={secondCol}>
<div className="float-left w-75" style={{ width: width ?? '' }}>
<CInput
id={id}
type="text"
required
value={localValue}
onChange={handleTyping}
invalid={field.error}
disabled={disabled}
placeholder={placeholder}
/>
</div>
<div className="float-left pl-3">{extraButton}</div>
<CInvalidFeedback>{errorMessage}</CInvalidFeedback>
</CCol>
</CFormGroup>
);
}
return (
<CFormGroup row className="py-1">
<CLabel col sm={firstCol} htmlFor="name">
@@ -56,7 +87,6 @@ const StringField = ({
onChange={handleTyping}
invalid={field.error}
disabled={disabled}
maxLength="50"
placeholder={placeholder}
/>
</div>
@@ -77,6 +107,7 @@ StringField.propTypes = {
disabled: PropTypes.bool.isRequired,
width: PropTypes.string,
placeholder: PropTypes.string,
extraButton: PropTypes.node,
};
StringField.defaultProps = {
@@ -85,6 +116,7 @@ StringField.defaultProps = {
errorMessage: '',
width: null,
placeholder: null,
extraButton: null,
};
export default StringField;

View File

@@ -0,0 +1,88 @@
import React, { useState } from 'react';
import ReactTooltip from 'react-tooltip';
import { v4 as createUuid } from 'uuid';
import { CButton, CCardBody, CCardHeader, CRow, CCol, CPopover, CButtonClose } from '@coreui/react';
import { cilTrash } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import PropTypes from 'prop-types';
import LoadingButton from '../LoadingButton';
import styles from './index.module.scss';
const DeleteButton = ({ t, contact, deleteContact, hideTooltips }) => {
const [tooltipId] = useState(createUuid());
return (
<CPopover content={t('common.delete')}>
<div className="d-inline">
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
data-tip
data-for={tooltipId}
data-event="click"
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-trash" content={cilTrash} size="sm" />
</CButton>
<ReactTooltip
id={tooltipId}
place="top"
effect="solid"
globalEventOff="click"
clickable
className={[styles.deleteTooltip, 'tooltipRight'].join(' ')}
border
borderColor="#321fdb"
arrowColor="white"
overridePosition={({ left, top }) => {
const element = document.getElementById(tooltipId);
const tooltipWidth = element ? element.offsetWidth : 0;
const newLeft = left - tooltipWidth * 0.4;
return { top, left: newLeft };
}}
>
<CCardHeader color="primary" className={styles.tooltipHeader}>
{t('inventory.delete_tag')}
<CButtonClose
style={{ color: 'white' }}
onClick={(e) => {
e.target.parentNode.parentNode.classList.remove('show');
hideTooltips();
}}
/>
</CCardHeader>
<CCardBody>
<CRow>
<CCol>
<LoadingButton
data-toggle="dropdown"
variant="outline"
color="danger"
label={t('common.confirm')}
isLoadingLabel={t('user.deleting')}
isLoading={false}
action={() => deleteContact(contact.id)}
block
disabled={false}
/>
</CCol>
</CRow>
</CCardBody>
</ReactTooltip>
</div>
</CPopover>
);
};
DeleteButton.propTypes = {
t: PropTypes.func.isRequired,
contact: PropTypes.instanceOf(Object).isRequired,
deleteContact: PropTypes.func.isRequired,
hideTooltips: PropTypes.func.isRequired,
};
export default DeleteButton;

View File

@@ -0,0 +1,90 @@
import React, { useState } from 'react';
import ReactTooltip from 'react-tooltip';
import { v4 as createUuid } from 'uuid';
import { CButton, CCardBody, CCardHeader, CRow, CCol, CPopover, CButtonClose } from '@coreui/react';
import { cilMinus } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import PropTypes from 'prop-types';
import LoadingButton from '../LoadingButton';
import styles from './index.module.scss';
const UnassignButton = ({ t, contact, unassignContact, hideTooltips, disabled }) => {
const [tooltipId] = useState(createUuid());
return (
<CPopover content={t('inventory.unassign')}>
<div className="d-inline">
<CButton
disabled={disabled}
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
data-tip
data-for={tooltipId}
data-event="click"
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-minus" content={cilMinus} size="sm" />
</CButton>
<ReactTooltip
id={tooltipId}
place="top"
effect="solid"
globalEventOff="click"
clickable
className={[styles.unassignTooltip, 'tooltipRight'].join(' ')}
border
borderColor="#321fdb"
arrowColor="white"
overridePosition={({ left, top }) => {
const element = document.getElementById(tooltipId);
const tooltipWidth = element ? element.offsetWidth : 0;
const newLeft = left - tooltipWidth * 0.4;
return { top, left: newLeft };
}}
>
<CCardHeader color="primary" className={styles.tooltipHeader}>
{t('inventory.unassign_tag')}
<CButtonClose
style={{ color: 'white' }}
onClick={(e) => {
e.target.parentNode.parentNode.classList.remove('show');
hideTooltips();
}}
/>
</CCardHeader>
<CCardBody>
<CRow>
<CCol>
<LoadingButton
data-toggle="dropdown"
variant="outline"
color="danger"
label={t('common.confirm')}
isLoadingLabel={t('user.deleting')}
isLoading={false}
action={() => unassignContact(contact.serialNumber)}
block
disabled={false}
/>
</CCol>
</CRow>
</CCardBody>
</ReactTooltip>
</div>
</CPopover>
);
};
UnassignButton.propTypes = {
t: PropTypes.func.isRequired,
contact: PropTypes.instanceOf(Object).isRequired,
unassignContact: PropTypes.func.isRequired,
hideTooltips: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
};
export default UnassignButton;

View File

@@ -0,0 +1,181 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { CButton, CDataTable, CLink, CPopover, CButtonToolbar } from '@coreui/react';
import { cilPencil, cilPlus } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import ReactTooltip from 'react-tooltip';
import DeleteButton from './DeleteButton';
import UnassignButton from './UnassignButton';
import FormattedDate from '../FormattedDate';
const ContactTable = ({
t,
loading,
entity,
filterOnEntity,
contacts,
unassign,
assignToEntity,
toggleEditModal,
deleteContact,
perPageSwitcher,
pageSwitcher,
}) => {
const columns = filterOnEntity
? [
{ key: 'primaryEmail', label: t('contact.primary_email'), _style: { width: '15%' } },
{ key: 'name', label: t('user.name'), _style: { width: '10%' } },
{ key: 'description', label: t('user.description'), _style: { width: '20%' } },
{ key: 'created', label: t('common.created'), _style: { width: '10%' } },
{ key: 'actions', label: t('actions.actions'), _style: { width: '1%' } },
]
: [
{ key: 'primaryEmail', label: t('contact.primary_email'), _style: { width: '15%' } },
{ key: 'name', label: t('user.name'), _style: { width: '10%' } },
{ key: 'description', label: t('user.description'), _style: { width: '20%' } },
{ key: 'entity', label: t('entity.entity'), _style: { width: '10%' } },
{ key: 'created', label: t('common.created'), _style: { width: '12%' } },
{ key: 'modified', label: t('common.modified'), _style: { width: '12%' } },
{ key: 'actions', label: t('actions.actions'), _style: { width: '1%' } },
];
const hideTooltips = () => ReactTooltip.hide();
const escFunction = (event) => {
if (event.keyCode === 27) {
hideTooltips();
}
};
useEffect(() => {
document.addEventListener('keydown', escFunction, false);
return () => {
document.removeEventListener('keydown', escFunction, false);
};
}, []);
return (
<>
<CDataTable
addTableClasses="ignore-overflow"
items={contacts}
fields={columns}
hover
border
loading={loading}
scopedSlots={{
name: (item) => (
<td className="align-middle">
{item.firstname} {item.lastname}
</td>
),
created: (item) => (
<td className="align-middle">
<FormattedDate date={item.created} />
</td>
),
modified: (item) => (
<td className="align-middle">
<FormattedDate date={item.modified} />
</td>
),
description: (item) => <td className="align-middle">{item.description}</td>,
entity: (item) => (
<td className="align-middle">
{filterOnEntity ? (
item.entity
) : (
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/entity/${item.entity}`}
>
{item.extendedInfo?.entity?.name ?? item.entity}
</CLink>
)}
</td>
),
actions: (item) => (
<td className="text-center align-middle py-0">
<CButtonToolbar
role="group"
className="justify-content-flex-end"
style={{ width: '190px' }}
>
<CPopover content={t('inventory.assign_ent_ven')}>
<div>
<CButton
disabled={entity === null || filterOnEntity}
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
onClick={() => assignToEntity(item.id)}
style={{ width: '33px', height: '30px' }}
>
<CIcon content={cilPlus} />
</CButton>
</div>
</CPopover>
<UnassignButton
t={t}
contact={item}
unassignTag={unassign}
hideTooltips={hideTooltips}
disabled={entity === null || !entity.isVenue}
/>
<DeleteButton
t={t}
contact={item}
deleteContact={deleteContact}
hideTooltips={hideTooltips}
/>
<CPopover content="Edit Tag">
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="ml-2"
onClick={() => toggleEditModal(item.id)}
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-pencil" content={cilPencil} size="sm" />
</CButton>
</CPopover>
</CButtonToolbar>
</td>
),
}}
/>
<div className="pl-3">
{pageSwitcher}
<p className="float-left pr-2 pt-1">{t('common.items_per_page')}</p>
{perPageSwitcher}
</div>
</>
);
};
ContactTable.propTypes = {
t: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
entity: PropTypes.instanceOf(Object),
filterOnEntity: PropTypes.bool,
contacts: PropTypes.instanceOf(Array).isRequired,
unassign: PropTypes.func.isRequired,
assignToEntity: PropTypes.func.isRequired,
toggleEditModal: PropTypes.func.isRequired,
deleteContact: PropTypes.func.isRequired,
perPageSwitcher: PropTypes.node.isRequired,
pageSwitcher: PropTypes.node.isRequired,
};
ContactTable.defaultProps = {
filterOnEntity: false,
entity: null,
};
export default ContactTable;

View File

@@ -0,0 +1,28 @@
.unassignTooltip {
opacity: 1 !important;
padding: 0px 0px 0px 0px !important;
border-radius: 1rem !important;
background-color: #fff !important;
border-color: #321fdb !important;
font-size: 0.875rem !important;
font-weight: 400 !important;
box-shadow: 0 3px 10px rgb(0 0 0 / 0.2) !important;
width: 300px;
}
.deleteTooltip {
opacity: 1 !important;
padding: 0px 0px 0px 0px !important;
border-radius: 1rem !important;
background-color: #fff !important;
border-color: #321fdb !important;
font-size: 0.875rem !important;
font-weight: 400 !important;
box-shadow: 0 3px 10px rgb(0 0 0 / 0.2) !important;
width: 200px;
}
.tooltipHeader {
border-top-left-radius: 1rem !important;
border-top-right-radius: 1rem !important;
}

View File

@@ -46,8 +46,11 @@ const CreateUserForm = ({ t, fields, updateField, policies, toggleChange }) => {
</CLabel>
<CCol sm="4">
<CSelect custom id="userRole" defaultValue="Admin" onChange={updateField}>
<option value="accounting">Accounting</option>
<option value="admin">Admin</option>
<option value="csr">CSR</option>
<option value="installer">Installer</option>
<option value="noc">NOC</option>
<option value="root">Root</option>
<option value="special">Special</option>
<option value="sub">Sub</option>

View File

@@ -85,10 +85,10 @@ const EditConfigurationForm = ({
<CFormGroup row>
<CCol>
<CRow className="pb-0">
<CLabel md="3" col htmlFor="name">
<CLabel lg="5" xl="3" col htmlFor="name">
<div>{t('user.name')}:</div>
</CLabel>
<CCol md="9">
<CCol lg="7" xl="9">
{editing ? (
<div>
<CInput
@@ -111,10 +111,10 @@ const EditConfigurationForm = ({
</CCol>
</CRow>
<CRow className="pb-0">
<CLabel md="3" col htmlFor="name">
<CLabel lg="5" xl="3" col htmlFor="name">
<div>{t('user.description')}:</div>
</CLabel>
<CCol md="9">
<CCol lg="7" xl="9">
{editing ? (
<div>
<CInput
@@ -135,10 +135,10 @@ const EditConfigurationForm = ({
</CCol>
</CRow>
<CRow className="pt-1">
<CLabel md="3" col htmlFor="name">
<CLabel lg="5" xl="3" col htmlFor="name">
<div>{t('configuration.device_types')}:</div>
</CLabel>
<CCol md="9">
<CCol lg="7" xl="9">
<Select
isMulti
closeMenuOnSelect={false}
@@ -156,10 +156,10 @@ const EditConfigurationForm = ({
</CCol>
</CRow>
<CRow className="pt-1 pb-0">
<CLabel md="3" col htmlFor="name">
<CLabel lg="5" xl="3" col htmlFor="name">
<div>RRM:</div>
</CLabel>
<CCol md="9">
<CCol lg="7" xl="9">
<div style={{ width: '120px' }}>
<Select
id="rrm"
@@ -179,10 +179,10 @@ const EditConfigurationForm = ({
</CCol>
</CRow>
<CRow className="py-1">
<CLabel col md="4" xxl="3" htmlFor="firmwareUpgrade">
<CLabel col lg="5" xl="3" htmlFor="firmwareUpgrade">
Firmware Upgrade
</CLabel>
<CCol md="8" xxl="9">
<CCol lg="7" xl="9">
<div style={{ width: '120px' }}>
<Select
id="rrm"
@@ -202,10 +202,10 @@ const EditConfigurationForm = ({
</CCol>
</CRow>
<CRow className="py-1">
<CLabel col md="3" htmlFor="firmwareRCOnly">
<CLabel col lg="5" xl="3" htmlFor="firmwareRCOnly">
Only Release Candidates
</CLabel>
<CCol xxl="9">
<CCol lg="7" xl="9">
<CSwitch
id="firmwareRCOnly"
color="primary"
@@ -219,10 +219,10 @@ const EditConfigurationForm = ({
</CCol>
</CRow>
<CRow className="pb-0">
<CLabel md="3" col htmlFor="name">
<CLabel lg="5" xl="3" col htmlFor="name">
<div>{t('configuration.used_by')}:</div>
</CLabel>
<CCol md="9">
<CCol lg="7" xl="9">
<CButton
disabled={fields.inUse.value.length === 0}
className="ml-0 pl-0"
@@ -236,20 +236,20 @@ const EditConfigurationForm = ({
</CCol>
<CCol className="mt-2">
<CRow className="pb-0">
<CLabel md="3" col htmlFor="name">
<CLabel lg="5" xl="3" col htmlFor="name">
<div>{t('common.created')}:</div>
</CLabel>
<CCol md="9">
<CCol lg="7" xl="9">
<p className="mt-2 mb-0">
<FormattedDate date={fields.created.value} />
</p>
</CCol>
</CRow>
<CRow className="pb-0">
<CLabel md="3" col htmlFor="name">
<CLabel lg="5" xl="3" col htmlFor="name">
<div>{t('common.modified')}:</div>
</CLabel>
<CCol md="9">
<CCol lg="7" xl="9">
<p className="mt-2 mb-0">
<FormattedDate date={fields.modified.value} />
</p>

View File

@@ -0,0 +1,363 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Select from 'react-select';
import CreatableSelect from 'react-select/creatable';
import {
CForm,
CInput,
CLabel,
CCol,
CFormGroup,
CInvalidFeedback,
CFormText,
CRow,
CDataTable,
CLink,
CPopover,
CButton,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilPlus } from '@coreui/icons';
import FormattedDate from '../FormattedDate';
import NotesTable from '../NotesTable';
const EditContactForm = ({
t,
disable,
fields,
updateField,
updateFieldWithKey,
entities,
addNote,
batchSetField,
hideEntities,
}) => {
const [filter, setFilter] = useState('');
const onPhonesChange = (v) => updateFieldWithKey('phones', { value: v.map((obj) => obj.value) });
const onMobilesChange = (v) =>
updateFieldWithKey('mobiles', { value: v.map((obj) => obj.value) });
const NoOptionsMessage = () => (
<h6 className="text-center pt-2">{t('common.type_for_options')}</h6>
);
const columns = [
{ key: 'created', label: t('common.created'), _style: { width: '20%' }, filter: false },
{ key: 'name', label: t('user.name'), _style: { width: '25%' }, filter: false },
{ key: 'description', label: t('user.description'), _style: { width: '50%' } },
{ key: 'actions', label: '', _style: { width: '15%' }, filter: false },
];
const selectEntity = ({ id, name }) => {
batchSetField([
{ id: 'entity', value: id },
{ id: 'entityName', value: name },
]);
};
return (
<CForm>
<CRow>
<CLabel className="mb-5" sm="2" col htmlFor="type">
{t('contact.type')}
</CLabel>
<CCol sm="4">
<div style={{ width: '200px' }}>
<Select
id="type"
value={
fields.type.value !== ''
? { value: fields.type.value, label: fields.type.value }
: null
}
onChange={(v) => updateFieldWithKey('type', { value: v.value })}
options={[
{ label: 'SUBSCRIBER', value: 'SUBSCRIBER' },
{ label: 'USER', value: 'USER' },
{ label: 'INSTALLER', value: 'INSTALLER' },
{ label: 'CSR', value: 'CSR' },
{ label: 'MANAGER', value: 'MANAGER' },
{ label: 'BUSINESSOWNER', value: 'BUSINESSOWNER' },
{ label: 'TECHNICIAN', value: 'TECHNICIAN' },
{ label: 'CORPORATE', value: 'CORPORATE' },
]}
isDisabled={disable}
/>
</div>
<CFormText color={fields.type.error ? 'danger' : ''}>{t('common.required')}</CFormText>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="title">
{t('contact.user_title')}
</CLabel>
<CCol sm="4">
<CInput
id="title"
type="text"
required
value={fields.title.value}
onChange={updateField}
invalid={fields.title.error}
disabled={disable}
maxLength="50"
/>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="salutation">
{t('contact.salutation')}
</CLabel>
<CCol sm="4">
<div style={{ width: '120px' }}>
<Select
id="salutation"
value={{
value: fields.salutation.value,
label: fields.salutation.value === '' ? 'None' : fields.salutation.value,
}}
onChange={(v) => updateFieldWithKey('salutation', { value: v.value })}
options={[
{ label: 'None', value: '' },
{ label: 'Mr.', value: 'Mr.' },
{ label: 'Ms.', value: 'Ms.' },
{ label: 'Mx.', value: 'Mx.' },
{ label: 'Dr.', value: 'Dr.' },
]}
isDisabled={disable}
/>
</div>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="accessPIN">
{t('contact.access_pin')}
</CLabel>
<CCol sm="4">
<CInput
id="accessPIN"
type="text"
required
value={fields.accessPIN.value}
onChange={updateField}
invalid={fields.accessPIN.error}
disabled={disable}
maxLength="50"
/>
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="firstname">
{t('contact.first_name')}
</CLabel>
<CCol sm="4">
<CInput
id="firstname"
type="text"
required
value={fields.firstname.value}
onChange={updateField}
invalid={fields.firstname.error}
disabled={disable}
maxLength="50"
/>
<CFormText color={fields.firstname.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="lastname">
{t('contact.last_name')}
</CLabel>
<CCol sm="4">
<CInput
id="lastname"
type="text"
required
value={fields.lastname.value}
onChange={updateField}
invalid={fields.lastname.error}
disabled={disable}
maxLength="50"
/>
<CFormText color={fields.lastname.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="initials">
{t('contact.initials')}
</CLabel>
<CCol sm="4">
<CInput
id="initials"
type="text"
required
value={fields.initials.value}
onChange={updateField}
disabled={disable}
maxLength="50"
/>
</CCol>
<CLabel sm="2" col htmlFor="visual">
{t('contact.visual')}
</CLabel>
<CCol sm="4">
<CInput
id="visual"
type="text"
required
value={fields.visual.value}
onChange={updateField}
disabled={disable}
maxLength="50"
/>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="primaryEmail">
{t('contact.primary_email')}
</CLabel>
<CCol sm="4">
<CInput
id="primaryEmail"
type="text"
required
value={fields.primaryEmail.value}
onChange={updateField}
invalid={fields.primaryEmail.error}
disabled={disable}
maxLength="50"
/>
<CFormText color={fields.primaryEmail.error ? 'danger' : ''}>
{t('common.required')}
</CFormText>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="secondaryEmail">
{t('contact.secondary_email')}
</CLabel>
<CCol sm="4">
<CInput
id="secondaryEmail"
type="text"
required
value={fields.secondaryEmail.value}
onChange={updateField}
invalid={fields.secondaryEmail.error}
disabled={disable}
maxLength="50"
/>
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="phones">
Landlines
</CLabel>
<CCol sm="4">
<CreatableSelect
isMulti
id="phones"
isDisabled={disable}
onChange={onPhonesChange}
components={{ NoOptionsMessage }}
options={[]}
value={fields.phones.value.map((opt) => ({ value: opt, label: opt }))}
placeholder={t('common.type_for_options')}
/>
</CCol>
<CLabel className="mb-5" sm="2" col htmlFor="phones">
Mobiles
</CLabel>
<CCol sm="4">
<CreatableSelect
id="mobiles"
isMulti
isDisabled={disable}
onChange={onMobilesChange}
components={{ NoOptionsMessage }}
options={[]}
value={fields.mobiles.value.map((opt) => ({ value: opt, label: opt }))}
placeholder={t('common.type_for_options')}
/>
</CCol>
<CLabel sm="2" col htmlFor="description">
{t('user.description')}
</CLabel>
<CCol sm="4">
<CInput
id="description"
type="text"
required
value={fields.description.value}
onChange={updateField}
disabled={disable}
maxLength="50"
/>
</CCol>
</CRow>
<CFormGroup row className="pb-1">
<CLabel sm="2" col htmlFor="title">
{t('entity.selected_entity')}
</CLabel>
<CCol sm="4" className="pt-2">
<h6>
{fields.entity.value === '' ? t('entity.need_select_entity') : fields.entityName.value}
</h6>
</CCol>
</CFormGroup>
{hideEntities ? null : (
<div className="overflow-auto border mb-3" style={{ height: '200px' }}>
<CInput
className="w-50 mb-2"
type="text"
placeholder="Search"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<CDataTable
items={entities}
fields={columns}
hover
tableFilterValue={filter}
border
scopedSlots={{
name: (item) => (
<td>
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/entity/${item.id}`}
>
{item.name}
</CLink>
</td>
),
created: (item) => (
<td>
<FormattedDate date={item.created} />
</td>
),
actions: (item) => (
<td>
<CPopover content={t('entity.select_entity')}>
<CButton color="primary" variant="outline" onClick={() => selectEntity(item)}>
<CIcon content={cilPlus} />
</CButton>
</CPopover>
</td>
),
}}
/>
</div>
)}
<NotesTable t={t} notes={fields.notes.value} addNote={addNote} loading={disable} editable />
</CForm>
);
};
EditContactForm.propTypes = {
t: PropTypes.func.isRequired,
disable: PropTypes.bool.isRequired,
fields: PropTypes.instanceOf(Object).isRequired,
updateField: PropTypes.func.isRequired,
updateFieldWithKey: PropTypes.func.isRequired,
entities: PropTypes.instanceOf(Array).isRequired,
addNote: PropTypes.func.isRequired,
batchSetField: PropTypes.func.isRequired,
hideEntities: PropTypes.bool,
};
EditContactForm.defaultProps = {
hideEntities: false,
};
export default EditContactForm;

View File

@@ -31,10 +31,10 @@ const EditEntityForm = ({
<CFormGroup row>
<CCol>
<CRow className="pb-0">
<CLabel md="4" xxl="3" col htmlFor="name">
<CLabel lg="5" xl="3" col htmlFor="name">
<div>{t('user.name')}:</div>
</CLabel>
<CCol md="8" xxl="9">
<CCol lg="7" xxl="9">
{editing ? (
<div>
<CInput
@@ -57,10 +57,10 @@ const EditEntityForm = ({
</CCol>
</CRow>
<CRow className="pb-0">
<CLabel md="4" xxl="3" col htmlFor="name">
<CLabel lg="5" xl="3" col htmlFor="name">
<div>{t('user.description')}:</div>
</CLabel>
<CCol md="8" xxl="9">
<CCol lg="7" xxl="9">
{editing ? (
<div>
<CInput
@@ -81,10 +81,10 @@ const EditEntityForm = ({
</CCol>
</CRow>
<CRow className="pb-0">
<CLabel md="4" xxl="3" col htmlFor="name">
<CLabel lg="5" xl="3" col htmlFor="name">
<div>RRM:</div>
</CLabel>
<CCol md="8" xxl="9">
<CCol lg="7" xxl="9">
<div style={{ width: '120px' }}>
<Select
id="rrm"
@@ -101,10 +101,10 @@ const EditEntityForm = ({
</CCol>
</CRow>
<CRow className="pb-0">
<CLabel md="4" xxl="3" col htmlFor="name">
<CLabel lg="5" xl="3" col htmlFor="name">
<div>{t('configuration.title')}:</div>
</CLabel>
<CCol md="8" xxl="9">
<CCol lg="7" xxl="9">
{editing ? (
<CButton className="pl-0 text-left" color="link" onClick={toggleAssociate}>
{fields.deviceConfiguration.value === ''
@@ -129,10 +129,10 @@ const EditEntityForm = ({
</CCol>
</CRow>
<CRow className="pb-0">
<CLabel md="4" xxl="3" col htmlFor="sourceIp">
<CLabel lg="5" xl="3" col htmlFor="sourceIp">
<div>{t('entity.ip_detection')}:</div>
</CLabel>
<CCol md="8" xxl="9">
<CCol lg="7" xxl="9">
{editing ? (
<CButton className="pl-0 text-left" color="link" onClick={toggleIpModal}>
{fields.sourceIP.value.length === 0
@@ -151,10 +151,10 @@ const EditEntityForm = ({
</CCol>
</CRow>
<CRow className="pb-0">
<CLabel md="4" xxl="3" col htmlFor="name">
<CLabel lg="5" xl="3" col htmlFor="name">
<div>{t('common.created')}:</div>
</CLabel>
<CCol md="8" xxl="9">
<CCol lg="7" xxl="9">
<div className="mt-2 mb-0">
<FormattedDate date={fields.created.value} />
</div>
@@ -163,10 +163,10 @@ const EditEntityForm = ({
</CCol>
<CCol className="mt-1">
<CRow className="pb-0">
<CLabel md="4" xxl="3" col htmlFor="name">
<CLabel lg="5" xl="3" col htmlFor="name">
<div>{t('common.modified')}:</div>
</CLabel>
<CCol md="8" xxl="9">
<CCol lg="7" xxl="9">
<div className="mt-2 mb-0">
<FormattedDate date={fields.modified.value} />
</div>

View File

@@ -16,15 +16,19 @@ import {
CInputFile,
} from '@coreui/react';
import PropTypes from 'prop-types';
import parsePhoneNumber from 'libphonenumber-js';
import CIcon from '@coreui/icons-react';
import ValidatePhoneNumberModal from 'components/ValidatePhoneNumberModal';
import NotesTable from '../NotesTable';
import LoadingButton from '../LoadingButton';
import Avatar from '../Avatar';
import useToggle from '../../hooks/useToggle';
const EditMyProfile = ({
t,
user,
updateUserWithId,
updateWithKey,
loading,
policies,
addNote,
@@ -33,26 +37,49 @@ const EditMyProfile = ({
deleteAvatar,
showPreview,
fileInputKey,
sendPhoneNumberTest,
testVerificationCode,
}) => {
const [showPhoneModal, togglePhoneModal] = useToggle(false);
const [showPassword, setShowPassword] = useState(false);
const toggleShowPassword = () => {
setShowPassword(!showPassword);
};
const saveNumber = (newNumber) => {
const newUserTypeInfo = { ...user.userTypeProprietaryInfo.value };
newUserTypeInfo.mobiles[0] = { number: `+${newNumber}` };
updateWithKey('userTypeProprietaryInfo', newUserTypeInfo);
};
const parseNumber = () => {
if (user.userTypeProprietaryInfo.value.mobiles?.length > 0) {
const phoneNumber = parsePhoneNumber(
`${user.userTypeProprietaryInfo.value.mobiles[0].number}`,
);
if (phoneNumber) {
return phoneNumber.formatInternational();
}
}
return t('user.add_phone_number');
};
return (
<CForm>
<CFormGroup row>
<CLabel sm="2" col htmlFor="name">
<CLabel lg="2" xxl="1" col htmlFor="name">
{t('user.name')}
</CLabel>
<CCol sm="4">
<CCol lg="4" xxl="5">
<CInput id="name" value={user.name.value} onChange={updateUserWithId} maxLength="20" />
</CCol>
<CLabel sm="2" col htmlFor="description">
<CLabel lg="2" xxl="1" col htmlFor="description">
{t('user.description')}
</CLabel>
<CCol sm="4">
<CCol lg="4" xxl="5">
<CInput
id="description"
value={user.description.value}
@@ -62,11 +89,17 @@ const EditMyProfile = ({
</CCol>
</CFormGroup>
<CFormGroup row>
<CLabel sm="2" col htmlFor="userRole">
<CLabel lg="2" xxl="1" col htmlFor="userRole">
{t('user.user_role')}
</CLabel>
<CCol sm="4">
<CSelect custom id="userRole" onChange={updateUserWithId} value={user.userRole.value}>
<CCol lg="4" xxl="5">
<CSelect
custom
id="userRole"
onChange={updateUserWithId}
value={user.userRole.value}
style={{ width: '100px' }}
>
<option value="admin">Admin</option>
<option value="csr">CSR</option>
<option value="root">Root</option>
@@ -75,10 +108,10 @@ const EditMyProfile = ({
<option value="system">System</option>
</CSelect>
</CCol>
<CLabel sm="2" col htmlFor="currentPassword">
<CLabel lg="2" xxl="1" col htmlFor="currentPassword">
{t('login.new_password')}
</CLabel>
<CCol sm="4">
<CCol lg="4" xxl="5">
<CInputGroup>
<CInput
type={showPassword ? 'text' : 'password'}
@@ -103,25 +136,42 @@ const EditMyProfile = ({
</CCol>
</CFormGroup>
<CFormGroup row>
<CCol sm="6">
<NotesTable
t={t}
notes={user.notes.value}
addNote={addNote}
loading={loading}
size="lg"
/>
<CLabel lg="2" xxl="1" col htmlFor="userRole">
{t('user.user_role')}
</CLabel>
<CCol lg="4" xxl="5">
<CSelect
custom
id="mfaMethod"
onChange={updateUserWithId}
value={user.mfaMethod.value}
style={{ width: '100px' }}
>
<option value="">Off</option>
<option value="sms">SMS</option>
<option value="email">Email</option>
</CSelect>
</CCol>
<CLabel sm="2" col htmlFor="avatar">
<CLabel lg="2" xxl="1" col htmlFor="name">
{t('user.phone_number')}
</CLabel>
<CCol lg="4" xxl="5">
<CButton color="link" onClick={togglePhoneModal} className="pl-0">
{parseNumber()}
</CButton>
</CCol>
</CFormGroup>
<CFormGroup row>
<CLabel lg="2" xl="1" col htmlFor="avatar" className="pt-2">
{t('user.avatar')}
</CLabel>
<CCol sm="4">
<CCol lg="10" xl="5" className="pt-2">
<CRow>
<CCol sm="3" className="pt-2">
<CCol lg="2" xl="2" className="pt-2">
{t('common.current')}
<div className="pt-5">Preview</div>
</CCol>
<CCol sm="2">
<CCol lg="1" xl="1">
<Avatar src={avatar} fallback={user.email.value} />
<div className="pt-3">
<Avatar src={newAvatar} fallback={user.email.value} />
@@ -150,6 +200,15 @@ const EditMyProfile = ({
</CCol>
</CRow>
</CCol>
<CCol lg="12" xl="6" className="pt-2">
<NotesTable
t={t}
notes={user.notes.value}
addNote={addNote}
loading={loading}
size="lg"
/>
</CCol>
</CFormGroup>
<CRow>
<CCol />
@@ -165,6 +224,14 @@ const EditMyProfile = ({
</CLink>
</CCol>
</CRow>
<ValidatePhoneNumberModal
t={t}
show={showPhoneModal}
toggle={togglePhoneModal}
sendPhoneNumberTest={sendPhoneNumberTest}
save={saveNumber}
testVerificationCode={testVerificationCode}
/>
</CForm>
);
};
@@ -181,6 +248,9 @@ EditMyProfile.propTypes = {
showPreview: PropTypes.func.isRequired,
deleteAvatar: PropTypes.func.isRequired,
fileInputKey: PropTypes.number.isRequired,
sendPhoneNumberTest: PropTypes.func.isRequired,
testVerificationCode: PropTypes.func.isRequired,
updateWithKey: PropTypes.func.isRequired,
};
EditMyProfile.defaultProps = {

View File

@@ -53,8 +53,11 @@ const EditUserForm = ({ t, user, updateUserWithId, loading, policies, addNote })
</CLabel>
<CCol sm="4">
<CSelect custom id="userRole" onChange={updateUserWithId} value={user.userRole.value}>
<option value="accounting">Accounting</option>
<option value="admin">Admin</option>
<option value="csr">CSR</option>
<option value="installer">Installer</option>
<option value="noc">NOC</option>
<option value="root">Root</option>
<option value="special">Special</option>
<option value="sub">Sub</option>

View File

@@ -0,0 +1,134 @@
import React, { useEffect, useState } from 'react';
import CIcon from '@coreui/icons-react';
import { cilCloudUpload, cilSave, cilTrash, cilX } from '@coreui/icons';
import PropTypes from 'prop-types';
import {
CButton,
CPopover,
CModal,
CModalBody,
CModalHeader,
CModalTitle,
CInputFile,
CAlert,
} from '@coreui/react';
import useToggle from '../../hooks/useToggle';
const validatePem = (value) =>
(value.includes('---BEGIN CERTIFICATE---') && value.includes('---END CERTIFICATE---')) ||
(value.includes('---BEGIN PRIVATE KEY---') && value.includes('---END PRIVATE KEY---'));
const FileToStringButton = ({ t, save, title, explanations, acceptedFileTypes, size }) => {
const [show, toggle] = useToggle(false);
const [value, setValue] = useState('');
const [fileName, setFileName] = useState('');
const [fileError, setFileError] = useState(false);
const [key, setKey] = useState(0);
let fileReader;
const saveValue = () => {
save(value, fileName);
toggle();
};
const deleteValue = () => {
save('', '');
toggle();
};
const clear = () => {
setValue('');
setFileError(false);
setFileName('');
setKey(key + 1);
};
const handleFileRead = () => {
setFileError(false);
const content = fileReader.result;
if (content && validatePem(content)) {
setValue(window.btoa(content));
} else {
setFileError(true);
}
};
const handleFile = (file) => {
fileReader = new FileReader();
if (file?.name !== undefined) setFileName(file.name);
fileReader.onloadend = handleFileRead;
fileReader.readAsText(file);
};
useEffect(() => {
if (show) clear();
}, [show]);
return (
<div>
<CPopover content="Import from PEM file">
<CButton
onClick={toggle}
color="primary"
variant="outline"
style={{ height: '35px', width: '35px' }}
size={size}
>
<CIcon content={cilCloudUpload} />
</CButton>
</CPopover>
<CModal size="lg" show={show} onClose={toggle}>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{title}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.save')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={saveValue}>
<CIcon content={cilSave} />
</CButton>
</CPopover>
<CPopover content={t('common.delete')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={deleteValue}>
<CIcon content={cilTrash} />
</CButton>
</CPopover>
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
<h6>{explanations}</h6>
<CInputFile
className="my-3"
key={key}
id="file-input"
name="file-input"
accept={acceptedFileTypes}
onChange={(e) => handleFile(e.target.files[0])}
/>
<CAlert className="my-3" hidden={!fileError} color="danger">
{acceptedFileTypes === '.pem' ? t('common.invalid_pem') : t('common.invalid_file')}
</CAlert>
</CModalBody>
</CModal>
</div>
);
};
FileToStringButton.propTypes = {
t: PropTypes.func.isRequired,
save: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
explanations: PropTypes.string.isRequired,
acceptedFileTypes: PropTypes.string.isRequired,
size: PropTypes.string,
};
FileToStringButton.defaultProps = {
size: 'md',
};
export default React.memo(FileToStringButton);

View File

@@ -1,8 +1,8 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import ReactPaginate from 'react-paginate';
import { CButton, CDataTable, CLink, CPopover, CButtonToolbar, CSelect } from '@coreui/react';
import { cilPencil, cilPlus, cilSpreadsheet } from '@coreui/icons';
import { cilPencil, cilPlus, cilRouter, cilSpreadsheet } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import ReactTooltip from 'react-tooltip';
import DeleteButton from './DeleteButton';
@@ -28,7 +28,9 @@ const InventoryTable = ({
toggleAssociate,
toggleAssocEntity,
toggleComputed,
pushConfig,
}) => {
const [gwUi] = useState(localStorage.getItem('owgw-ui'));
const columns =
onlyEntity || onlyUnassigned
? [
@@ -75,7 +77,19 @@ const InventoryTable = ({
border
loading={loading}
scopedSlots={{
serialNumber: (item) => <td className="align-middle">{item.serialNumber}</td>,
serialNumber: (item) => (
<td className="align-middle">
<CLink
className="c-subheader-nav-link align-self-center"
aria-current="page"
href={`${gwUi}/#/devices/${item.serialNumber}`}
target="_blank"
disabled={!gwUi || gwUi === ''}
>
{item.serialNumber}
</CLink>
</td>
),
name: (item) => <td className="align-middle">{item.name}</td>,
created: (item) => (
<td className="align-middle">
@@ -97,9 +111,9 @@ const InventoryTable = ({
})
}
>
{item.deviceConfigurationName === ''
{item.deviceConfiguration === ''
? t('configuration.add_or_link')
: item.deviceConfigurationName}
: item.extendedInfo?.deviceConfiguration?.name ?? item.deviceConfiguration}
</CButton>
</td>
),
@@ -113,7 +127,7 @@ const InventoryTable = ({
aria-current="page"
to={() => `/entity/${item.entity}`}
>
{item.entity_info ? item.entity_info.name : item.entity}
{item.extendedInfo?.entity?.name ?? item.entity}
</CLink>
)}
</td>
@@ -128,7 +142,7 @@ const InventoryTable = ({
aria-current="page"
to={() => `/venue/${item.venue}`}
>
{item.venue_info ? item.venue_info.name : item.venue}
{item.extendedInfo?.venue?.name ?? item.venue}
</CLink>
)}
</td>
@@ -138,7 +152,7 @@ const InventoryTable = ({
<CButtonToolbar
role="group"
className="justify-content-flex-end"
style={{ width: '250px' }}
style={{ width: '300px' }}
>
<CPopover content={t('inventory.assign_ent_ven')}>
<div>
@@ -167,6 +181,20 @@ const InventoryTable = ({
hideTooltips={hideTooltips}
/>
<DeleteButton t={t} tag={item} deleteTag={deleteTag} hideTooltips={hideTooltips} />
<CPopover content="Push Configuration to Device">
<CButton
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-2"
onClick={() => pushConfig(item.serialNumber)}
style={{ width: '33px', height: '30px' }}
disabled={item.deviceConfigurationName === ''}
>
<CIcon name="cil-router" content={cilRouter} size="sm" />
</CButton>
</CPopover>
<CPopover content="See Computed Configuration">
<CButton
color="primary"
@@ -255,6 +283,7 @@ InventoryTable.propTypes = {
toggleAssociate: PropTypes.func.isRequired,
toggleAssocEntity: PropTypes.func.isRequired,
toggleComputed: PropTypes.func.isRequired,
pushConfig: PropTypes.func.isRequired,
};
InventoryTable.defaultProps = {

View File

@@ -0,0 +1,114 @@
import React, { useState } from 'react';
import { CButton, CCol, CForm, CInput, CRow, CSpinner, CAlert, CLink } from '@coreui/react';
import PropTypes from 'prop-types';
import LanguageSwitcher from '../LanguageSwitcher';
import styles from './index.module.scss';
const AccountVerificationForm = ({
t,
i18n,
onKeyDown,
toggleForgotPassword,
validateVerificationCode,
resendValidationCode,
policies,
formType,
}) => {
const [sending, setSending] = useState(false);
const [validating, setValidating] = useState(false);
const [code, setCode] = useState('');
const [success, setSuccess] = useState(null);
const onChange = (e) => setCode(e.target.value);
const resendCode = () => {
setSending(true);
resendValidationCode().finally(() => setSending(false));
};
const validate = () => {
setValidating(true);
validateVerificationCode(code)
.then((result) => setSuccess(result))
.finally(() => setValidating(false));
};
return (
<CForm onKeyDown={(e) => onKeyDown(e, validateVerificationCode)}>
<h1>
{t('login.account_verification')}
<div className={styles.languageSwitcher}>
<LanguageSwitcher i18n={i18n} />
</div>
</h1>
<p className="text-muted">
{formType.split('-')[1] === 'sms'
? t('login.phone_validation_explanation')
: t('login.email_code_validation')}
</p>
<div className="d-flex flex-row">
<CInput
autoFocus
required
type="text"
value={code}
placeholder={t('login.verification_code')}
onChange={onChange}
/>
<CButton
className="ml-4"
style={{ width: '300px' }}
color="primary"
onClick={resendCode}
disabled={sending || validating}
>
{sending ? t('login.sending_ellipsis') : t('user.send_code_again')}
<CSpinner hidden={!sending} color="light" component="span" size="sm" />
</CButton>
</div>
<CRow className="pt-2">
<CCol>
<CAlert show={success !== null} color={success ? 'success' : 'danger'}>
{t('login.wrong_code')}
</CAlert>
</CCol>
</CRow>
<CRow className="pt-2">
<CCol>
<CButton color="primary" className="px-4" onClick={validate} disabled={validating}>
{validating ? t('login.sending_ellipsis') : t('user.validate_phone')}
<CSpinner hidden={!validating} color="light" component="span" size="sm" />
</CButton>
<CLink
className="c-subheader-nav-link px-3"
aria-current="page"
href={policies.accessPolicy}
target="_blank"
hidden={policies.accessPolicy.length === 0}
>
{t('common.access_policy')}
</CLink>
</CCol>
<CCol xs="5" className={styles.forgotPassword}>
<CButton variant="ghost" color="primary" onClick={toggleForgotPassword}>
{t('common.back_to_login')}
</CButton>
</CCol>
</CRow>
</CForm>
);
};
AccountVerificationForm.propTypes = {
t: PropTypes.func.isRequired,
i18n: PropTypes.instanceOf(Object).isRequired,
onKeyDown: PropTypes.func.isRequired,
toggleForgotPassword: PropTypes.func.isRequired,
validateVerificationCode: PropTypes.func.isRequired,
policies: PropTypes.instanceOf(Object).isRequired,
resendValidationCode: PropTypes.func.isRequired,
formType: PropTypes.string.isRequired,
};
export default React.memo(AccountVerificationForm);

View File

@@ -106,37 +106,33 @@ const LoginForm = ({
</CAlert>
</CCol>
</CRow>
<CRow>
<CCol>
<CButton color="primary" className="px-4" onClick={signIn} disabled={loading}>
{loading ? t('login.logging_in') : t('login.login')}
<CSpinner hidden={!loading} color="light" component="span" size="sm" />
</CButton>
<CLink
className="c-subheader-nav-link px-3"
aria-current="page"
href={policies.accessPolicy}
target="_blank"
hidden={policies.accessPolicy.length === 0}
>
{t('common.access_policy')}
</CLink>
<CLink
className="c-subheader-nav-link"
aria-current="page"
href={policies.passwordPolicy}
target="_blank"
hidden={policies.passwordPolicy.length === 0}
>
{t('common.password_policy')}
</CLink>
</CCol>
<CCol xs="5" className="text-right">
<CButton variant="ghost" color="primary" onClick={toggleForgotPassword}>
{t('common.forgot_password')}
</CButton>
</CCol>
</CRow>
<div className="d-flex flex-row align-middle">
<CButton color="primary" className="px-4" onClick={signIn} disabled={loading}>
{loading ? t('login.logging_in') : t('login.login')}
<CSpinner hidden={!loading} color="light" component="span" size="sm" />
</CButton>
<CLink
className="c-subheader-nav-link px-3 align-self-center"
aria-current="page"
href={policies.accessPolicy}
target="_blank"
hidden={policies.accessPolicy.length === 0}
>
{t('common.access_policy')}
</CLink>
<CLink
className="c-subheader-nav-link align-self-center"
aria-current="page"
href={policies.passwordPolicy}
target="_blank"
hidden={policies.passwordPolicy.length === 0}
>
{t('common.password_policy')}
</CLink>
<CButton className="ml-auto" variant="ghost" color="primary" onClick={toggleForgotPassword}>
{t('common.forgot_password')}
</CButton>
</div>
</CForm>
);

View File

@@ -5,6 +5,7 @@ import styles from './index.module.scss';
import LoginForm from './LoginForm';
import ForgotPasswordForm from './ForgotPasswordForm';
import ChangePasswordForm from './ChangePasswordForm';
import AccountVerificationForm from './AccountVerificationForm';
const LoginPage = ({
t,
@@ -16,17 +17,32 @@ const LoginPage = ({
forgotResponse,
fields,
updateField,
isLogin,
isPasswordChange,
formType,
toggleForgotPassword,
onKeyDown,
sendForgotPasswordEmail,
changePasswordResponse,
cancelPasswordChange,
policies,
validateCode,
resendValidationCode,
}) => {
const getForm = () => {
if (!isLogin) {
if (formType.split('-')[0] === 'validation') {
return (
<AccountVerificationForm
t={t}
i18n={i18n}
onKeyDown={onKeyDown}
toggleForgotPassword={toggleForgotPassword}
validateVerificationCode={validateCode}
resendValidationCode={resendValidationCode}
policies={policies}
formType={formType}
/>
);
}
if (formType === 'forgot-password') {
return (
<ForgotPasswordForm
t={t}
@@ -43,7 +59,7 @@ const LoginPage = ({
/>
);
}
if (isPasswordChange) {
if (formType === 'change-password') {
return (
<ChangePasswordForm
t={t}
@@ -86,7 +102,7 @@ const LoginPage = ({
alt="OpenWifi"
/>
<CCardGroup>
<CCard className="p-4" color={isLogin && isPasswordChange ? 'secondary' : ''}>
<CCard className="p-4" color={formType === 'change-password' ? 'secondary' : ''}>
<CCardBody>{getForm()}</CCardBody>
</CCard>
</CCardGroup>
@@ -107,14 +123,15 @@ LoginPage.propTypes = {
forgotResponse: PropTypes.instanceOf(Object).isRequired,
fields: PropTypes.instanceOf(Object).isRequired,
updateField: PropTypes.func.isRequired,
isLogin: PropTypes.bool.isRequired,
isPasswordChange: PropTypes.bool.isRequired,
toggleForgotPassword: PropTypes.func.isRequired,
onKeyDown: PropTypes.func.isRequired,
sendForgotPasswordEmail: PropTypes.func.isRequired,
changePasswordResponse: PropTypes.instanceOf(Object).isRequired,
cancelPasswordChange: PropTypes.func.isRequired,
policies: PropTypes.instanceOf(Object).isRequired,
formType: PropTypes.string.isRequired,
validateCode: PropTypes.func.isRequired,
resendValidationCode: PropTypes.func.isRequired,
};
export default React.memo(LoginPage);

View File

@@ -12,9 +12,10 @@ import {
} from '@coreui/react';
import { cilBan, cilCheckCircle, cilPencil, cilPlus, cilSync, cilTrash } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import { capitalizeFirstLetter, prettyDate } from '../../utils/formatting';
import { capitalizeFirstLetter } from '../../utils/formatting';
import DeleteModal from '../DeleteModal';
import Avatar from '../Avatar';
import FormattedDate from '../FormattedDate';
const UserListTable = ({
t,
@@ -115,7 +116,9 @@ const UserListTable = ({
</td>
),
lastLogin: (item) => (
<td className="align-middle">{item.lastLogin ? prettyDate(item.lastLogin) : ''}</td>
<td className="align-middle">
{item.lastLogin ? <FormattedDate date={item.lastLogin} /> : ''}
</td>
),
validated: (item) => (
<td className="text-center align-middle">

View File

@@ -0,0 +1,166 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import PhoneInput from 'react-phone-input-2';
import 'react-phone-input-2/lib/bootstrap.css';
import parsePhoneNumber from 'libphonenumber-js';
import {
CModal,
CModalBody,
CModalHeader,
CModalTitle,
CPopover,
CButton,
CRow,
CCol,
CInput,
CAlert,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { cilSave, cilX } from '@coreui/icons';
const ValidatePhoneNumberModal = ({
t,
show,
toggle,
save,
sendPhoneNumberTest,
testVerificationCode,
}) => {
const [loading, setLoading] = useState(false);
const [sent, setSent] = useState(false);
const [phoneNumber, setPhoneNumber] = useState(null);
const [code, setCode] = useState('');
const [phoneValidated, setPhoneValidated] = useState(false);
const onCodeChange = (e) => {
setPhoneValidated(false);
setCode(e.target.value);
};
const sendTest = async () => {
setLoading(true);
setSent(false);
sendPhoneNumberTest(`+${phoneNumber}`)
.then((result) => setSent(result))
.finally(setLoading(false));
};
const testCode = async () => {
testVerificationCode(`+${phoneNumber}`, code).then((result) => {
setPhoneValidated(result);
});
};
const validateRawPhoneNumber = () => {
if (phoneNumber !== null) {
const numberTest = parsePhoneNumber(`+${phoneNumber}`);
if (numberTest) {
return numberTest.isValid();
}
}
return false;
};
const saveNumber = () => {
toggle();
save(phoneNumber);
};
useEffect(() => {
if (show) {
setPhoneNumber(null);
setLoading(false);
setSent(false);
setCode('');
setPhoneValidated(false);
}
}, [show]);
return (
<CModal show={show} onClose={toggle}>
<CModalHeader className="p-1">
<CModalTitle className="pl-1 pt-1">{t('user.phone_number')}</CModalTitle>
<div className="text-right">
<CPopover content={t('common.save')}>
<CButton
color="primary"
variant="outline"
className="ml-2"
onClick={saveNumber}
disabled={!phoneValidated}
>
<CIcon content={cilSave} />
</CButton>
</CPopover>
<CPopover content={t('common.close')}>
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
<CIcon content={cilX} />
</CButton>
</CPopover>
</div>
</CModalHeader>
<CModalBody>
<CRow>
<CCol>
<h6>{t('user.enter_new_phone')}</h6>
</CCol>
</CRow>
<CRow>
<CCol sm="8">
<PhoneInput
enableSearch
value={phoneNumber}
onChange={setPhoneNumber}
disabled={loading}
/>
</CCol>
<CCol>
<CButton
color="primary"
onClick={sendTest}
className="mt-1"
disabled={!validateRawPhoneNumber() || loading}
>
{t('user.send_code')}
</CButton>
</CCol>
</CRow>
<h6 hidden={!sent} className="pt-4">
{t('user.check_phone')}
</h6>
<CRow hidden={!sent}>
<CCol sm="3">
<CInput
style={{ width: '110px' }}
type="text"
value={code}
onChange={onCodeChange}
maxLength="6"
placeholder="Enter code"
/>
</CCol>
<CCol sm="3">
<CButton color="primary" onClick={testCode}>
{t('user.validate_phone')}
</CButton>
</CCol>
</CRow>
<CAlert hidden={!phoneValidated} className="mt-4" color="success">
{t('user.successful_validation')}
</CAlert>
</CModalBody>
</CModal>
);
};
ValidatePhoneNumberModal.propTypes = {
t: PropTypes.func.isRequired,
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
save: PropTypes.func.isRequired,
sendPhoneNumberTest: PropTypes.func.isRequired,
testVerificationCode: PropTypes.func.isRequired,
};
export default ValidatePhoneNumberModal;

View File

@@ -13,6 +13,7 @@ import {
CLink,
CPopover,
} from '@coreui/react';
import { useHistory } from 'react-router-dom';
import { cilPencil, cilPlus, cilSync } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import DeleteButton from './DeleteButton';
@@ -33,6 +34,8 @@ const VenueTable = ({
deleteVenue,
refresh,
}) => {
const history = useHistory();
const columns = [
{ key: 'name', label: t('user.name'), _style: { width: '20%' } },
{ key: 'description', label: t('user.description'), _style: { width: '25%' } },
@@ -121,13 +124,12 @@ const VenueTable = ({
>
<CPopover content="Edit Tag">
<CButton
disabled
color="primary"
variant="outline"
shape="square"
size="sm"
className="mx-1"
onClick={() => {}}
onClick={() => history.push(`/venue/${item.id}`)}
style={{ width: '33px', height: '30px' }}
>
<CIcon name="cil-pencil" content={cilPencil} size="sm" />

View File

@@ -22,6 +22,8 @@ export const AuthProvider = ({ axiosInstance, token, apiEndpoints, children }) =
.then(() => {})
.catch(() => {})
.finally(() => {
localStorage.removeItem('access_token');
localStorage.removeItem('gateway_endpoints');
sessionStorage.clear();
window.location.replace('/');
});

View File

@@ -2,6 +2,7 @@
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import { v4 as createUuid } from 'uuid';
import { set as lodashSet, get as lodashGet } from 'lodash';
import { useAuth } from '../AuthProvider';
@@ -14,6 +15,7 @@ const navbarOption = ({
children,
childrenEntities,
childrenVenues,
extraData = {},
}) => {
let tag = 'SidebarChildless';
if (children) tag = 'SidebarDropdown';
@@ -25,7 +27,8 @@ const navbarOption = ({
name,
path,
isVenue,
onClick: () => selectEntity(uuid, name, path, isVenue, childrenEntities, childrenVenues),
onClick: () =>
selectEntity(uuid, name, path, isVenue, childrenEntities, childrenVenues, extraData),
_children: children,
};
};
@@ -42,7 +45,7 @@ export const EntityProvider = ({ axiosInstance, selectedEntity, children }) => {
const [parentsWithChildrenLoaded, setParentsWithChildrenLoaded] = useState([]);
const [deviceTypes, setDeviceTypes] = useState([]);
const selectEntity = (uuid, name, path, isVenue, childrenEntities, childrenVenues) => {
const selectEntity = (uuid, name, path, isVenue, childrenEntities, childrenVenues, extraData) => {
// If we have not yet gotten the information of this entity's children, we get them now
if (childrenEntities || childrenVenues) {
setEntityToRetrieve({ childrenEntities, childrenVenues, path, uuid, isVenue });
@@ -54,6 +57,7 @@ export const EntityProvider = ({ axiosInstance, selectedEntity, children }) => {
childrenEntities,
childrenVenues,
path,
extraData,
});
history.push(`/${isVenue ? 'venue' : 'entity'}/${uuid}`);
};
@@ -98,7 +102,7 @@ export const EntityProvider = ({ axiosInstance, selectedEntity, children }) => {
const setProviderEntity = async (id, isVenue) => {
const newEntity = await getEntity(id, isVenue);
if (newEntity) {
setEntity({
const newObj = {
...newEntity,
uuid: newEntity.id,
name: newEntity.name,
@@ -106,7 +110,11 @@ export const EntityProvider = ({ axiosInstance, selectedEntity, children }) => {
isVenue,
childrenIds: newEntity.children,
childrenVenues: newEntity.venues,
});
extraData: newEntity,
refreshId: createUuid(),
};
setEntity({ ...newObj });
}
};
@@ -162,6 +170,7 @@ export const EntityProvider = ({ axiosInstance, selectedEntity, children }) => {
children: nestedOptions,
childrenEntities: grandChildrenEntities,
childrenVenues: grandChildrenVenues,
extraData: result,
});
});

View File

@@ -8,6 +8,11 @@ const hasError = (e, field) => {
return `This field has a minimum value of ${field.minimum}`;
if (field.maximum !== undefined && e.target.value > field.maximum)
return `This field has a maximum value of ${field.maximum}`;
} else if (field.type && field.type === 'string') {
if (field.minLength && e.target.value.length < field.minLength)
return `This field has a minimum length of ${field.minLength}`;
if (field.maxLength && e.target.value.length > field.maxLength)
return `This field has a maximum length of ${field.maxLength}`;
}
return null;
};
@@ -44,5 +49,15 @@ export default (initialState) => {
setFields({ ...formFields });
} else if (force) setFields({ ...formFields });
},
(fieldsList) => {
const newFields = { ...fields };
fieldsList.forEach((field) => {
const oldField = lodashGet(newFields, field.id);
lodashSet(newFields, field.id, { ...oldField, value: field.value });
});
setFields({ ...newFields });
},
];
};

View File

@@ -9,6 +9,7 @@ export { default as Sidebar } from './layout/Sidebar';
// Components
export { default as AddConfigurationForm } from './components/AddConfigurationForm';
export { default as AddContactForm } from './components/AddContactForm';
export { default as AddEntityForm } from './components/AddEntityForm';
export { default as AddInventoryTagForm } from './components/AddInventoryTagForm';
export { default as AddVenueForm } from './components/AddVenueForm';
@@ -16,6 +17,7 @@ export { default as ApiStatusCard } from './components/ApiStatusCard';
export { default as Avatar } from './components/Avatar';
export { default as ConfigurationCustomMultiModal } from './components/Configuration/CustomMultiModal';
export { default as ConfigurationElement } from './components/Configuration/ConfigurationElement';
export { default as ConfigurationFileField } from './components/Configuration/FileField';
export { default as ConfigurationInUseModal } from './components/ConfigurationInUseModal';
export { default as ConfigurationIntField } from './components/Configuration/IntField';
export { default as ConfigurationMulti } from './components/Configuration/Multi';
@@ -25,6 +27,7 @@ export { default as ConfigurationSelect } from './components/Configuration/Selec
export { default as ConfigurationStringField } from './components/Configuration/StringField';
export { default as ConfigurationToggle } from './components/Configuration/Toggle';
export { default as ConfirmFooter } from './components/ConfirmFooter';
export { default as ContactTable } from './components/ContactTable';
export { default as CopyToClipboardButton } from './components/CopyToClipboardButton';
export { default as CreateUserForm } from './components/CreateUserForm';
export { default as ConfigurationTable } from './components/ConfigurationTable';
@@ -33,6 +36,7 @@ export { default as DeviceListTable } from './components/DeviceListTable';
export { default as DeviceStatusCard } from './components/DeviceStatusCard';
export { default as DeviceSearchBar } from './components/DeviceSearchBar';
export { default as EditConfigurationForm } from './components/EditConfigurationForm';
export { default as EditContactForm } from './components/EditContactForm';
export { default as EditEntityForm } from './components/EditEntityForm';
export { default as EditInventoryTagForm } from './components/EditInventoryTagForm';
export { default as EditMyProfile } from './components/EditMyProfile';
@@ -41,6 +45,7 @@ export { default as EditUserModal } from './components/EditUserModal';
export { default as EditVenueForm } from './components/EditVenueForm';
export { default as EventQueueModal } from './components/EventQueueModal';
export { default as InventoryTable } from './components/InventoryTable';
export { default as FileToStringButton } from './components/FileToStringButton';
export { default as FirmwareHistoryTable } from './components/FirmwareHistoryTable';
export { default as FirmwareList } from './components/FirmwareList';
export { default as FormattedDate } from './components/FormattedDate';