From 12998ac6c06fb849c0ef2da7957b621b5043ef15 Mon Sep 17 00:00:00 2001 From: James Nuanez Date: Wed, 22 Apr 2020 13:01:08 -0700 Subject: [PATCH] Implement bulk edit phone numbers --- src/components/blocks/TableContent.js | 142 +++++++++++++++--- src/components/blocks/TableMenu.js | 62 ++------ src/components/elements/Button.js | 104 +++++++++++++ src/components/elements/Checkbox.js | 10 +- src/components/elements/Table.js | 15 +- .../pages/internal/PhoneNumbersList.js | 62 +++++++- 6 files changed, 318 insertions(+), 77 deletions(-) diff --git a/src/components/blocks/TableContent.js b/src/components/blocks/TableContent.js index 0c85fcf..44edc61 100644 --- a/src/components/blocks/TableContent.js +++ b/src/components/blocks/TableContent.js @@ -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" /> )} - +
+ {/* colgroup is used to set the width of the last column because the + last two + + + - {props.columns.map(c => ( - + )} + {props.columns.map((c, i) => ( + ))} - {showTableLoader ? ( - @@ -186,6 +281,17 @@ const TableContent = props => { ) : ( content.map(a => ( + {props.withCheckboxes && ( + + )} {props.columns.map((c, i) => (
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) */} +
+ {props.withCheckboxes && ( + + onClick={checkboxesToggleAll} + /> + + {selected.length && i === props.columns.length - 1 ? ( +
+ ({ + name: i.name, + type: 'button', + action: () => handleBulkAction(selected, i), + })) + } + /> +
+ ) : ( + + )}
+
+ + {i === 0 diff --git a/src/components/blocks/TableMenu.js b/src/components/blocks/TableMenu.js index 68cf4e6..a91aff2 100644 --- a/src/components/blocks/TableMenu.js +++ b/src/components/blocks/TableMenu.js @@ -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 => ( {props.open && ( - + {props.menuItems.map((m, i) => ( m.type === 'link' ? diff --git a/src/components/elements/Button.js b/src/components/elements/Button.js index 5eb77a6..d2ba3bc 100644 --- a/src/components/elements/Button.js +++ b/src/components/elements/Button.js @@ -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) => { diff --git a/src/components/elements/Checkbox.js b/src/components/elements/Checkbox.js index ea1dae3..8ac4208 100644 --- a/src/components/elements/Checkbox.js +++ b/src/components/elements/Checkbox.js @@ -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 ( { type="checkbox" checked={props.checked} onChange={props.onChange} + value={props.value} ref={inputRef} /> 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; diff --git a/src/components/pages/internal/PhoneNumbersList.js b/src/components/pages/internal/PhoneNumbersList.js index 5f22c66..9638477 100644 --- a/src/components/pages/internal/PhoneNumbersList.js +++ b/src/components/pages/internal/PhoneNumbersList.js @@ -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" > );