diff --git a/src/short-urls/ShortUrlsList.js b/src/short-urls/ShortUrlsList.js index 3010aef1..460106d2 100644 --- a/src/short-urls/ShortUrlsList.js +++ b/src/short-urls/ShortUrlsList.js @@ -1,7 +1,7 @@ import { faCaretDown as caretDownIcon, faCaretUp as caretUpIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { head, isEmpty, keys, values } from 'ramda'; -import React from 'react'; +import React, { useState, useEffect } from 'react'; import qs from 'qs'; import PropTypes from 'prop-types'; import { EventSourcePolyfill as EventSource } from 'event-source-polyfill'; @@ -20,148 +20,130 @@ export const SORTABLE_FIELDS = { visits: 'Visits', }; +const propTypes = { + listShortUrls: PropTypes.func, + resetShortUrlParams: PropTypes.func, + shortUrlsListParams: shortUrlsListParamsType, + match: PropTypes.object, + location: PropTypes.object, + loading: PropTypes.bool, + error: PropTypes.bool, + shortUrlsList: PropTypes.arrayOf(shortUrlType), + selectedServer: serverType, + createNewVisit: PropTypes.func, + mercureInfo: MercureInfoType, +}; + // FIXME Replace with typescript: (ShortUrlsRow component) -const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Component { - static propTypes = { - listShortUrls: PropTypes.func, - resetShortUrlParams: PropTypes.func, - shortUrlsListParams: shortUrlsListParamsType, - match: PropTypes.object, - location: PropTypes.object, - loading: PropTypes.bool, - error: PropTypes.bool, - shortUrlsList: PropTypes.arrayOf(shortUrlType), - selectedServer: serverType, - createNewVisit: PropTypes.func, - mercureInfo: MercureInfoType, - }; - - refreshList = (extraParams) => { - const { listShortUrls, shortUrlsListParams } = this.props; - - listShortUrls({ - ...shortUrlsListParams, - ...extraParams, +const ShortUrlsList = (ShortUrlsRow) => { + const ShortUrlsListComp = ({ + listShortUrls, + resetShortUrlParams, + shortUrlsListParams, + match, + location, + loading, + error, + shortUrlsList, + selectedServer, + createNewVisit, + mercureInfo, + }) => { + const { orderBy } = shortUrlsListParams; + const [ order, setOrder ] = useState({ + orderField: orderBy && head(keys(orderBy)), + orderDir: orderBy && head(values(orderBy)), }); - }; - - handleOrderBy = (orderField, orderDir) => { - this.setState({ orderField, orderDir }); - this.refreshList({ orderBy: { [orderField]: orderDir } }); - }; - - orderByColumn = (columnName) => () => - this.handleOrderBy(columnName, determineOrderDir(columnName, this.state.orderField, this.state.orderDir)); - - renderOrderIcon = (field) => { - if (this.state.orderField !== field) { - return null; - } - - if (!this.state.orderDir) { - return null; - } - - return ( - - ); - }; - - constructor(props) { - super(props); - - const { orderBy } = props.shortUrlsListParams; - - this.state = { - orderField: orderBy ? head(keys(orderBy)) : undefined, - orderDir: orderBy ? head(values(orderBy)) : undefined, + const refreshList = (extraParams) => listShortUrls({ ...shortUrlsListParams, ...extraParams }); + const handleOrderBy = (orderField, orderDir) => { + setOrder({ orderField, orderDir }); + refreshList({ orderBy: { [orderField]: orderDir } }); }; - } + const orderByColumn = (columnName) => () => + handleOrderBy(columnName, determineOrderDir(columnName, order.orderField, order.orderDir)); + const renderOrderIcon = (field) => { + if (order.orderField !== field) { + return null; + } - componentDidMount() { - const { match: { params }, location, shortUrlsListParams } = this.props; - const query = qs.parse(location.search, { ignoreQueryPrefix: true }); - const tags = query.tag ? [ query.tag ] : shortUrlsListParams.tags; + if (!order.orderDir) { + return null; + } - this.refreshList({ page: params.page, tags }); - } - - componentDidUpdate() { - const { mercureHubUrl, token, loading, error } = this.props.mercureInfo; - - if (loading || error) { - return; - } - - const hubUrl = new URL(mercureHubUrl); - - hubUrl.searchParams.append('topic', 'https://shlink.io/new-visit'); - this.closeEventSource(); - this.es = new EventSource(hubUrl, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - this.es.onmessage = ({ data }) => this.props.createNewVisit(JSON.parse(data)); - } - - componentWillUnmount() { - const { resetShortUrlParams } = this.props; - - this.closeEventSource(); - resetShortUrlParams(); - } - - closeEventSource = () => { - if (this.es) { - this.es.close(); - this.es = undefined; - } - } - - renderShortUrls() { - const { shortUrlsList, selectedServer, loading, error, shortUrlsListParams } = this.props; - - if (error) { return ( - - Something went wrong while loading short URLs :( - + ); - } + }; + const renderShortUrls = () => { + if (error) { + return ( + + Something went wrong while loading short URLs :( + + ); + } - if (loading) { - return Loading...; - } + if (loading) { + return Loading...; + } - if (!loading && isEmpty(shortUrlsList)) { - return No results found; - } + if (!loading && isEmpty(shortUrlsList)) { + return No results found; + } - return shortUrlsList.map((shortUrl) => ( - - )); - } + return shortUrlsList.map((shortUrl) => ( + + )); + }; + + useEffect(() => { + const { params } = match; + const query = qs.parse(location.search, { ignoreQueryPrefix: true }); + const tags = query.tag ? [ query.tag ] : shortUrlsListParams.tags; + + refreshList({ page: params.page, tags }); + + return resetShortUrlParams; + }, []); + useEffect(() => { + const { mercureHubUrl, token, loading, error } = mercureInfo; + + if (loading || error) { + return undefined; + } + + const hubUrl = new URL(mercureHubUrl); + + hubUrl.searchParams.append('topic', 'https://shlink.io/new-visit'); + const es = new EventSource(hubUrl, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + // es.onmessage = pipe(JSON.parse, createNewVisit); + es.onmessage = ({ data }) => createNewVisit(JSON.parse(data)); + + return () => es.close(); + }, [ mercureInfo ]); - render() { return (
@@ -169,42 +151,46 @@ const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Compon - {this.renderShortUrls()} + {renderShortUrls()}
- {this.renderOrderIcon('dateCreated')} + {renderOrderIcon('dateCreated')} Created at - {this.renderOrderIcon('shortCode')} + {renderOrderIcon('shortCode')} Short URL - {this.renderOrderIcon('longUrl')} + {renderOrderIcon('longUrl')} Long URL Tags - {this.renderOrderIcon('visits')} Visits + {renderOrderIcon('visits')} Visits  
); - } + }; + + ShortUrlsListComp.propTypes = propTypes; + + return ShortUrlsListComp; }; export default ShortUrlsList; diff --git a/test/short-urls/ShortUrlsList.test.js b/test/short-urls/ShortUrlsList.test.js index 95017966..050238b9 100644 --- a/test/short-urls/ShortUrlsList.test.js +++ b/test/short-urls/ShortUrlsList.test.js @@ -71,10 +71,6 @@ describe('', () => { }); it('should render 6 table header cells with conditional order by icon', () => { - const orderDirOptionToIconMap = { - ASC: caretUpIcon, - DESC: caretDownIcon, - }; const getThElementForSortableField = (sortableField) => wrapper.find('table') .find('thead') .find('tr') @@ -82,24 +78,18 @@ describe('', () => { .filterWhere((e) => e.text().includes(SORTABLE_FIELDS[sortableField])); Object.keys(SORTABLE_FIELDS).forEach((sortableField) => { - const sortableThElementWrapper = getThElementForSortableField(sortableField); + expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(0); - expect(sortableThElementWrapper.find(FontAwesomeIcon)).toHaveLength(0); - - sortableThElementWrapper.simulate('click'); + getThElementForSortableField(sortableField).simulate('click'); expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(1); - expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon).prop('icon')).toEqual( - orderDirOptionToIconMap.ASC, - ); + expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon).prop('icon')).toEqual(caretUpIcon); - sortableThElementWrapper.simulate('click'); + getThElementForSortableField(sortableField).simulate('click'); expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(1); - expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon).prop('icon')).toEqual( - orderDirOptionToIconMap.DESC, - ); + expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon).prop('icon')).toEqual(caretDownIcon); - sortableThElementWrapper.simulate('click'); - expect(sortableThElementWrapper.find(FontAwesomeIcon)).toHaveLength(0); + getThElementForSortableField(sortableField).simulate('click'); + expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(0); }); }); });