Implement bulk edit phone numbers

This commit is contained in:
James Nuanez
2020-04-22 13:01:08 -07:00
parent ff427b83a2
commit 12998ac6c0
6 changed files with 318 additions and 77 deletions
+124 -18
View File
@@ -5,6 +5,7 @@ import { ModalStateContext } from '../../contexts/ModalContext';
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
import Table from '../elements/Table.js';
import Button from '../elements/Button.js';
import Checkbox from '../elements/Checkbox.js';
import TableMenu from '../blocks/TableMenu.js';
import Loader from '../blocks/Loader.js';
import Modal from '../blocks/Modal.js';
@@ -74,7 +75,45 @@ const TableContent = props => {
}, []);
//=============================================================================
// Handle Table Menu (i.e. clicking the 3 dots on right)
// Handle checkboxes
//=============================================================================
const [ selected, setSelected ] = useState([]);
const checkboxesToggleAll = e => {
if (content.length === selected.length) {
setSelected([]);
} else {
setSelected(content.map(c => c.sid));
}
};
const checkboxesToggleOne = e => {
const sid = e.target.value;
setSelected(prev => {
if (prev.includes(sid)) {
return prev.filter(p => p !== sid);
} else {
return [...prev, sid];
}
});
};
const handleBulkAction = async (selected, i) => {
setShowTableLoader(true);
const success = await props.bulkAction(selected, i);
if (success) {
const newContent = await props.getContent();
sortTableContent({ newContent });
setSelected([]);
dispatch({
type: 'ADD',
level: 'success',
message: 'Number routing updated',
});
}
setShowTableLoader(false);
};
//=============================================================================
// Handle Open Menus (i.e. bulk action menu or 3 dots on right of each row)
//=============================================================================
const [ menuOpen, setMenuOpen ] = useState(null);
useEffect(() => {
@@ -145,34 +184,90 @@ const TableContent = props => {
actionText="Delete"
/>
)}
<Table>
<Table withCheckboxes={props.withCheckboxes}>
{/* colgroup is used to set the width of the last column because the
last two <th> are combined in a colSpan="2", preventing the columns from
being given an expicit width (`table-layout: fixed;` requires setting
column width in the first row) */}
<colgroup>
<col
span={
props.withCheckboxes
? props.columns.length + 1
: props.columns.length
}
/>
<col style={{ width: '4rem' }}></col>
</colgroup>
<thead>
<tr>
{props.columns.map(c => (
<th key={c.key}>
{props.withCheckboxes && (
<th>
<Button
text
gray
tableHeaderLink
onClick={() => sortTableContent({ column: c.key })}
>
{c.header}
{sort.column === c.key
? sort.order === 'asc'
? <span>&#9652;</span>
: <span>&#9662;</span>
: null
checkbox={
!selected.length
? 'none'
: content.length === selected.length
? 'all'
: 'partial'
}
</Button>
onClick={checkboxesToggleAll}
/>
</th>
)}
{props.columns.map((c, i) => (
<th
key={c.key}
colSpan={i === props.columns.length - 1 ? '2' : null}
>
{selected.length && i === props.columns.length - 1 ? (
<div
style={{
position: 'relative',
display: 'inline-block',
marginLeft: '-1rem',
}}
>
<TableMenu
bulkEditMenu
buttonText="Choose Application"
sid="bulk-menu"
open={menuOpen === 'bulk-menu'}
handleMenuOpen={handleMenuOpen}
disabled={modalOpen}
menuItems={
props.bulkMenuItems.map(i => ({
name: i.name,
type: 'button',
action: () => handleBulkAction(selected, i),
}))
}
/>
</div>
) : (
<Button
text
gray
tableHeaderLink
onClick={() => sortTableContent({ column: c.key })}
>
{c.header}
{sort.column === c.key
? sort.order === 'asc'
? <span>&#9652;</span>
: <span>&#9662;</span>
: null
}
</Button>
)}
</th>
))}
<th></th>
</tr>
</thead>
<tbody>
{showTableLoader ? (
<tr>
<td colSpan="3">
<td colSpan={props.withCheckboxes ? props.columns.length + 1 : props.columns.length}>
<Loader height={'71px'} />
</td>
</tr>
@@ -186,6 +281,17 @@ const TableContent = props => {
) : (
content.map(a => (
<tr key={a.sid}>
{props.withCheckboxes && (
<td>
<Checkbox
forTable
id={a.sid}
value={a.sid}
onChange={checkboxesToggleOne}
checked={selected.includes(a.sid)}
/>
</td>
)}
{props.columns.map((c, i) => (
<td key={c.key}>
{i === 0
+15 -47
View File
@@ -2,52 +2,18 @@ import React from 'react';
import { Link } from 'react-router-dom';
import styled, { css } from 'styled-components/macro';
import { ReactComponent as MenuDots } from '../../images/MenuDots.svg';
const Button = styled.button`
background: none;
padding: 0;
border: 0;
outline: 0;
border-radius: 50%;
& > span {
background: ${props => props.selected
? '#E3E3E3'
: 'none'
};
height: 3rem;
width: 3rem;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
outline: 0;
fill: #767676;
cursor: pointer;
}
&:focus > span {
border: 2px solid #D91C5C;
background: ${props => props.selected
? 'RGBA(217, 28, 92, 0.15)'
: 'none'
};
fill: #D91C5C;
}
&:hover > span {
background: ${props => props.selected
? 'RGBA(217, 28, 92, 0.15)'
: 'none'
};
fill: #D91C5C;
}
`;
import Button from '../elements/Button';
const Container = styled.div`
position: absolute;
right: 1.75rem;
top: 3rem;
right: ${props => props.bulkEditMenu
? '0'
: '1.75rem'
};
top: ${props => props.bulkEditMenu
? 'calc(100% + 0.25rem)'
: '3rem'
};
display: flex;
flex-direction: column;
align-items: stretch;
@@ -99,6 +65,8 @@ const MenuButton = styled.button`
const TableMenu = props => (
<React.Fragment>
<Button
bulkEditMenu={props.bulkEditMenu}
tableMenu={!props.bulkEditMenu}
selected={props.open}
disabled={props.disabled}
onClick={e => {
@@ -106,12 +74,12 @@ const TableMenu = props => (
props.handleMenuOpen(props.sid);
}}
>
<span tabIndex="-1">
<MenuDots />
</span>
{props.buttonText || <MenuDots />}
</Button>
{props.open && (
<Container>
<Container
bulkEditMenu={props.bulkEditMenu}
>
{props.menuItems.map((m, i) => (
m.type === 'link'
? <MenuLink key={i} to={m.url}>
+104
View File
@@ -140,6 +140,110 @@ const StyledButton = styled.button`
cursor: not-allowed;
}
//=============================================================================
// Table Menu (3 dots on right of each row)
//=============================================================================
${props => props.tableMenu && `
border-radius: 50%;
overflow: hidden;
& > span {
background: ${props.selected
? '#E3E3E3'
: 'none'
};
height: 3rem;
width: 3rem;
border-radius: 50%;
outline: 0;
fill: #767676;
}
&:focus > span {
border: 2px solid #D91C5C;
background: ${props.selected
? 'RGBA(217, 28, 92, 0.15)'
: 'none'
};
fill: #D91C5C;
box-shadow: none;
}
&:hover:not([disabled]) > span,
&:active:not([disabled]) > span {
background: ${props.selected
? 'RGBA(217, 28, 92, 0.15)'
: 'none'
};
fill: #D91C5C;
}
`}
//=============================================================================
// "Check All" button for bulk editing in table
//=============================================================================
${props => props.checkbox && `
position: relative;
display: block;
& > span {
width: 1.5rem;
height: 1.5rem;
border: 1px solid #A5A5A5;
border-radius: 0.125rem;
background: #FFF;
padding: 0;
}
&:focus > span {
border-color: #565656;
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
}
&:hover:not([disabled]) > span,
&:active:not([disabled]) > span {
background: none;
}
`}
${props => (props.checkbox === 'all' || props.checkbox === 'partial') && `
& > span,
&:hover:not([disabled]) > span {
background: #D91C5C;
border-color: #D91C5C;
}
&:focus > span {
border: 3px solid #890934;
}
`}
${props => props.checkbox === 'all' && `
&::after {
content: '';
position: absolute;
top: 0.35rem;
left: 0.25rem;
height: 8px;
width: 15px;
border-left: 2px solid #FFF;
border-bottom: 2px solid #FFF;
transform: rotate(-45deg);
}
`}
${props => props.checkbox === 'partial' && `
&::after {
content: '';
position: absolute;
top: 0.6875rem;
left: 0.1875rem;
height: 0.125rem;
width: 1.125rem;
background: #FFF;
}
`}
`;
const Button = (props, ref) => {
+7 -3
View File
@@ -3,9 +3,11 @@ import styled from 'styled-components/macro';
import Label from './Label';
const CheckboxContainer = styled.div`
margin-left: ${props => props.invalid
? '0.5rem'
: '1rem'
margin-left: ${props => props.forTable
? '0'
: props.invalid
? '0.5rem'
: '1rem'
};
position: relative;
display: flex;
@@ -119,6 +121,7 @@ const Checkbox = (props, ref) => {
return (
<CheckboxContainer
invalid={props.invalid}
forTable={props.forTable}
>
<StyledCheckbox
id={props.id}
@@ -126,6 +129,7 @@ const Checkbox = (props, ref) => {
type="checkbox"
checked={props.checked}
onChange={props.onChange}
value={props.value}
ref={inputRef}
/>
<StyledLabel
+11 -4
View File
@@ -68,15 +68,22 @@ const Table = styled.table`
font-weight: bold;
}
& th:last-child {
width: 4rem;
}
& td:last-child {
overflow: inherit;
position: relative;
padding: 0.5rem;
}
${props => props.withCheckboxes && `
& th:first-child,
& td:first-child {
width: 3rem;
padding: 1.25rem 0 1.25rem 1.25rem;
}
& td:nth-child(2) {
font-weight: bold;
}
`}
`;
export default Table;
@@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React, { useState, useContext } from 'react';
import axios from 'axios';
import { NotificationDispatchContext } from '../../../contexts/NotificationContext';
import InternalTemplate from '../../templates/InternalTemplate';
@@ -55,6 +55,24 @@ const PhoneNumbersList = () => {
const applications = promiseAllValues[2].data;
const sipTrunks = promiseAllValues[3].data;
// sort all applications and store to state for use in bulk editing
const allApplications = [...applications, ];
allApplications.sort((a, b) => {
let valA = (a.name && a.name.toLowerCase()) || '';
let valB = (b.name && b.name.toLowerCase()) || '';
const result = valA > valB ? 1 : valA < valB ? -1 : 0;
return result;
});
const applicationsForBulk = allApplications.map(app => ({
name: app.name,
application_sid: app.application_sid,
}));
applicationsForBulk.push({
name: '- None -',
application_sid: null,
});
setApplications(applicationsForBulk);
const combinedData = phoneNumbers.map((p, i) => {
const account = accounts.filter(a => a.account_sid === p.account_sid );
const application = applications.filter(a => a.application_sid === p.application_sid );
@@ -111,6 +129,37 @@ const PhoneNumbersList = () => {
}
};
//=============================================================================
// Bulk Edit Applications
//=============================================================================
const [ applications, setApplications ] = useState([]);
const handleBulkEditApplications = async (phoneNumberSids, application) => {
try {
for (const sid of phoneNumberSids) {
await axios({
method: 'put',
baseURL: process.env.REACT_APP_API_BASE_URL,
url: `/PhoneNumbers/${sid}`,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
data: {
application_sid: application.application_sid,
}
});
}
return true;
} catch (err) {
dispatch({
type: 'ADD',
level: 'error',
message: (err.response && err.response.data && err.response.data.msg) || 'Something went wrong, please try again.',
});
console.log(err.response || err);
return false;
}
};
//=============================================================================
// Render
//=============================================================================
@@ -121,17 +170,20 @@ const PhoneNumbersList = () => {
addButtonLink="/internal/phone-numbers/add"
>
<TableContent
withCheckboxes
name="phone number"
urlParam="phone-numbers"
getContent={getPhoneNumbers}
columns={[
{ header: 'Number', key: 'number' },
{ header: 'SIP Trunk', key: 'sipTrunk' },
{ header: 'Account', key: 'account' },
{ header: 'Application', key: 'application' },
{ header: 'Number', key: 'number' },
{ header: 'SIP Trunk', key: 'sipTrunk' },
{ header: 'Account', key: 'account' },
{ header: 'Application', key: 'application' },
]}
formatContentToDelete={formatPhoneNumberToDelete}
deleteContent={deletePhoneNumber}
bulkMenuItems={applications}
bulkAction={handleBulkEditApplications}
/>
</InternalTemplate>
);