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

View file

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

View file

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

View file

@ -63,7 +63,7 @@ describe('<VisitsTable />', () => {
expect(paginator).toHaveLength(0);
});
it('selects a row when clicked', () => {
it('selected rows are highlighted', () => {
const wrapper = createWrapper(rangeOf(10, () => ({ userAgent: '', date: '', referer: '' })));
expect(wrapper.find('.text-primary')).toHaveLength(0);
@ -72,9 +72,19 @@ describe('<VisitsTable />', () => {
expect(wrapper.find('.text-primary')).toHaveLength(2);
expect(wrapper.find('.table-primary')).toHaveLength(1);
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('.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('.table-primary')).toHaveLength(0);
});