mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-10 18:27:25 +03:00
Allowed multiple selection on visits table
This commit is contained in:
parent
ca52911e42
commit
1c3119ee76
4 changed files with 63 additions and 40 deletions
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -17,11 +17,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.visits-table__header-cell--no-action {
|
||||
cursor: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.visits-table__header-icon {
|
||||
float: right;
|
||||
margin-top: 3px;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue