mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-07-04 19:21:58 +00:00
Implement bulk edit phone numbers
This commit is contained in:
@@ -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>▴</span>
|
||||
: <span>▾</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>▴</span>
|
||||
: <span>▾</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
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user