Allowed multiple selection on visits table

This commit is contained in:
Alejandro Celaya
2020-04-09 10:56:54 +02:00
parent ca52911e42
commit 1c3119ee76
4 changed files with 63 additions and 40 deletions

View File

@@ -31,7 +31,15 @@ const propTypes = {
matchMedia: PropTypes.func, matchMedia: PropTypes.func,
}; };
const highlightedVisitToStats = (highlightedVisit, prop) => highlightedVisit && { [highlightedVisit[prop]]: 1 }; const highlightedVisitsToStats = (highlightedVisits, prop) => highlightedVisits.reduce((acc, highlightedVisit) => {
if (!acc[highlightedVisit[prop]]) {
acc[highlightedVisit[prop]] = 0;
}
acc[highlightedVisit[prop]] += 1;
return acc;
}, {});
const format = formatDate(); const format = formatDate();
let memoizationId; let memoizationId;
let timeWhenMounted; let timeWhenMounted;
@@ -51,7 +59,7 @@ const ShortUrlVisits = ({ processStatsFromVisits }, OpenMapModalBtn) => {
const [ endDate, setEndDate ] = useState(undefined); const [ endDate, setEndDate ] = useState(undefined);
const [ showTable, toggleTable ] = useToggle(); const [ showTable, toggleTable ] = useToggle();
const [ tableIsSticky, , setSticky, unsetSticky ] = useToggle(); const [ tableIsSticky, , setSticky, unsetSticky ] = useToggle();
const [ highlightedVisit, setHighlightedVisit ] = useState(undefined); const [ highlightedVisits, setHighlightedVisits ] = useState([]);
const [ isMobileDevice, setIsMobileDevice ] = useState(false); const [ isMobileDevice, setIsMobileDevice ] = useState(false);
const determineIsMobileDevice = () => setIsMobileDevice(matchMedia('(max-width: 991px)').matches); const determineIsMobileDevice = () => setIsMobileDevice(matchMedia('(max-width: 991px)').matches);
@@ -124,7 +132,7 @@ const ShortUrlVisits = ({ processStatsFromVisits }, OpenMapModalBtn) => {
title="Referrers" title="Referrers"
stats={referrers} stats={referrers}
withPagination={false} withPagination={false}
highlightedStats={highlightedVisitToStats(highlightedVisit, 'referer')} highlightedStats={highlightedVisitsToStats(highlightedVisits, 'referer')}
sortingItems={{ sortingItems={{
name: 'Referrer name', name: 'Referrer name',
amount: 'Visits amount', amount: 'Visits amount',
@@ -135,7 +143,7 @@ const ShortUrlVisits = ({ processStatsFromVisits }, OpenMapModalBtn) => {
<SortableBarGraph <SortableBarGraph
title="Countries" title="Countries"
stats={countries} stats={countries}
highlightedStats={highlightedVisitToStats(highlightedVisit, 'country')} highlightedStats={highlightedVisitsToStats(highlightedVisits, 'country')}
sortingItems={{ sortingItems={{
name: 'Country name', name: 'Country name',
amount: 'Visits amount', amount: 'Visits amount',
@@ -146,7 +154,7 @@ const ShortUrlVisits = ({ processStatsFromVisits }, OpenMapModalBtn) => {
<SortableBarGraph <SortableBarGraph
title="Cities" title="Cities"
stats={cities} stats={cities}
highlightedStats={highlightedVisitToStats(highlightedVisit, 'city')} highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
extraHeaderContent={(activeCities) => extraHeaderContent={(activeCities) =>
mapLocations.length > 0 && mapLocations.length > 0 &&
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} /> <OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
@@ -198,7 +206,7 @@ const ShortUrlVisits = ({ processStatsFromVisits }, OpenMapModalBtn) => {
onEntered={setSticky} onEntered={setSticky}
onExiting={unsetSticky} onExiting={unsetSticky}
> >
<VisitsTable visits={visits} isSticky={tableIsSticky} onVisitSelected={setHighlightedVisit} /> <VisitsTable visits={visits} isSticky={tableIsSticky} onVisitsSelected={setHighlightedVisits} />
</Collapse> </Collapse>
)} )}

View File

@@ -19,7 +19,7 @@ import './VisitsTable.scss';
const propTypes = { const propTypes = {
visits: PropTypes.arrayOf(visitType).isRequired, visits: PropTypes.arrayOf(visitType).isRequired,
onVisitSelected: PropTypes.func, onVisitsSelected: PropTypes.func,
isSticky: PropTypes.bool, isSticky: PropTypes.bool,
matchMedia: PropTypes.func, matchMedia: PropTypes.func,
}; };
@@ -51,14 +51,14 @@ const normalizeVisits = map(({ userAgent, date, referer, visitLocation }) => ({
city: (visitLocation && visitLocation.cityName) || 'Unknown', city: (visitLocation && visitLocation.cityName) || 'Unknown',
})); }));
const VisitsTable = ({ visits, onVisitSelected, isSticky = false, matchMedia = window.matchMedia }) => { const VisitsTable = ({ visits, onVisitsSelected, isSticky = false, matchMedia = window.matchMedia }) => {
const allVisits = normalizeVisits(visits); const allVisits = normalizeVisits(visits);
const headerCellsClass = classNames('visits-table__header-cell', { const headerCellsClass = classNames('visits-table__header-cell', {
'visits-table__sticky': isSticky, 'visits-table__sticky': isSticky,
}); });
const matchMobile = () => matchMedia('(max-width: 767px)').matches; const matchMobile = () => matchMedia('(max-width: 767px)').matches;
const [ selectedVisit, setSelectedVisit ] = useState(undefined); const [ selectedVisits, setSelectedVisits ] = useState([]);
const [ isMobileDevice, setIsMobileDevice ] = useState(matchMobile()); const [ isMobileDevice, setIsMobileDevice ] = useState(matchMobile());
const [ searchTerm, setSearchTerm ] = useState(undefined); const [ searchTerm, setSearchTerm ] = useState(undefined);
const [ order, setOrder ] = useState({ field: undefined, dir: undefined }); const [ order, setOrder ] = useState({ field: undefined, dir: undefined });
@@ -77,8 +77,8 @@ const VisitsTable = ({ visits, onVisitSelected, isSticky = false, matchMedia = w
); );
useEffect(() => { useEffect(() => {
onVisitSelected && onVisitSelected(selectedVisit); onVisitsSelected && onVisitsSelected(selectedVisits);
}, [ selectedVisit ]); }, [ selectedVisits ]);
useEffect(() => { useEffect(() => {
const listener = () => setIsMobileDevice(matchMobile()); const listener = () => setIsMobileDevice(matchMobile());
@@ -88,6 +88,7 @@ const VisitsTable = ({ visits, onVisitSelected, isSticky = false, matchMedia = w
}, []); }, []);
useEffect(() => { useEffect(() => {
setPage(1); setPage(1);
setSelectedVisits([]);
}, [ searchTerm ]); }, [ searchTerm ]);
return ( return (
@@ -95,11 +96,14 @@ const VisitsTable = ({ visits, onVisitSelected, isSticky = false, matchMedia = w
<thead className="visits-table__header"> <thead className="visits-table__header">
<tr> <tr>
<th <th
className={classNames('visits-table__header-cell visits-table__header-cell--no-action', { className={classNames('visits-table__header-cell text-center', {
'visits-table__sticky': isSticky, 'visits-table__sticky': isSticky,
})} })}
onClick={() => setSelectedVisits(
selectedVisits.length < resultSet.total ? resultSet.visitsGroups.flat() : []
)}
> >
<FontAwesomeIcon icon={checkIcon} className={classNames({ 'text-primary': selectedVisit !== undefined })} /> <FontAwesomeIcon icon={checkIcon} className={classNames({ 'text-primary': selectedVisits.length > 0 })} />
</th> </th>
<th className={headerCellsClass} onClick={orderByColumn('date')}> <th className={headerCellsClass} onClick={orderByColumn('date')}>
Date Date
@@ -140,26 +144,32 @@ const VisitsTable = ({ visits, onVisitSelected, isSticky = false, matchMedia = w
</td> </td>
</tr> </tr>
)} )}
{resultSet.visitsGroups[page - 1] && resultSet.visitsGroups[page - 1].map((visit, index) => ( {resultSet.visitsGroups[page - 1] && resultSet.visitsGroups[page - 1].map((visit, index) => {
<tr const isSelected = selectedVisits.includes(visit);
key={index}
style={{ cursor: 'pointer' }} return (
className={classNames({ 'table-primary': selectedVisit === visit })} <tr
onClick={() => setSelectedVisit(selectedVisit === visit ? undefined : visit)} key={index}
> style={{ cursor: 'pointer' }}
<td className="text-center"> className={classNames({ 'table-primary': isSelected })}
{selectedVisit === visit && <FontAwesomeIcon icon={checkIcon} className="text-primary" />} onClick={() => setSelectedVisits(
</td> isSelected ? selectedVisits.filter((v) => v !== visit) : [ ...selectedVisits, visit ]
<td> )}
<Moment format="YYYY-MM-DD HH:mm">{visit.date}</Moment> >
</td> <td className="text-center">
<td>{visit.country}</td> {isSelected && <FontAwesomeIcon icon={checkIcon} className="text-primary" />}
<td>{visit.city}</td> </td>
<td>{visit.browser}</td> <td>
<td>{visit.os}</td> <Moment format="YYYY-MM-DD HH:mm">{visit.date}</Moment>
<td>{visit.referer}</td> </td>
</tr> <td>{visit.country}</td>
))} <td>{visit.city}</td>
<td>{visit.browser}</td>
<td>{visit.os}</td>
<td>{visit.referer}</td>
</tr>
);
})}
</tbody> </tbody>
{resultSet.total > PAGE_SIZE && ( {resultSet.total > PAGE_SIZE && (
<tfoot> <tfoot>

View File

@@ -17,11 +17,6 @@
} }
} }
.visits-table__header-cell--no-action {
cursor: auto;
text-align: center;
}
.visits-table__header-icon { .visits-table__header-icon {
float: right; float: right;
margin-top: 3px; margin-top: 3px;

View File

@@ -63,7 +63,7 @@ describe('<VisitsTable />', () => {
expect(paginator).toHaveLength(0); expect(paginator).toHaveLength(0);
}); });
it('selects a row when clicked', () => { it('selected rows are highlighted', () => {
const wrapper = createWrapper(rangeOf(10, () => ({ userAgent: '', date: '', referer: '' }))); const wrapper = createWrapper(rangeOf(10, () => ({ userAgent: '', date: '', referer: '' })));
expect(wrapper.find('.text-primary')).toHaveLength(0); expect(wrapper.find('.text-primary')).toHaveLength(0);
@@ -72,9 +72,19 @@ describe('<VisitsTable />', () => {
expect(wrapper.find('.text-primary')).toHaveLength(2); expect(wrapper.find('.text-primary')).toHaveLength(2);
expect(wrapper.find('.table-primary')).toHaveLength(1); expect(wrapper.find('.table-primary')).toHaveLength(1);
wrapper.find('tr').at(3).simulate('click'); wrapper.find('tr').at(3).simulate('click');
expect(wrapper.find('.text-primary')).toHaveLength(3);
expect(wrapper.find('.table-primary')).toHaveLength(2);
wrapper.find('tr').at(3).simulate('click');
expect(wrapper.find('.text-primary')).toHaveLength(2); expect(wrapper.find('.text-primary')).toHaveLength(2);
expect(wrapper.find('.table-primary')).toHaveLength(1); expect(wrapper.find('.table-primary')).toHaveLength(1);
wrapper.find('tr').at(3).simulate('click');
// Select all
wrapper.find('thead').find('th').at(0).simulate('click');
expect(wrapper.find('.text-primary')).toHaveLength(11);
expect(wrapper.find('.table-primary')).toHaveLength(10);
// Select none
wrapper.find('thead').find('th').at(0).simulate('click');
expect(wrapper.find('.text-primary')).toHaveLength(0); expect(wrapper.find('.text-primary')).toHaveLength(0);
expect(wrapper.find('.table-primary')).toHaveLength(0); expect(wrapper.find('.table-primary')).toHaveLength(0);
}); });