mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Converted short URLs list in functional component
This commit is contained in:
parent
f3129399de
commit
0f73cb9f8c
2 changed files with 130 additions and 154 deletions
|
@ -1,7 +1,7 @@
|
||||||
import { faCaretDown as caretDownIcon, faCaretUp as caretUpIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faCaretDown as caretDownIcon, faCaretUp as caretUpIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { head, isEmpty, keys, values } from 'ramda';
|
import { head, isEmpty, keys, values } from 'ramda';
|
||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
import { EventSourcePolyfill as EventSource } from 'event-source-polyfill';
|
||||||
|
@ -20,148 +20,130 @@ export const SORTABLE_FIELDS = {
|
||||||
visits: 'Visits',
|
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)
|
// FIXME Replace with typescript: (ShortUrlsRow component)
|
||||||
const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Component {
|
const ShortUrlsList = (ShortUrlsRow) => {
|
||||||
static propTypes = {
|
const ShortUrlsListComp = ({
|
||||||
listShortUrls: PropTypes.func,
|
listShortUrls,
|
||||||
resetShortUrlParams: PropTypes.func,
|
resetShortUrlParams,
|
||||||
shortUrlsListParams: shortUrlsListParamsType,
|
shortUrlsListParams,
|
||||||
match: PropTypes.object,
|
match,
|
||||||
location: PropTypes.object,
|
location,
|
||||||
loading: PropTypes.bool,
|
loading,
|
||||||
error: PropTypes.bool,
|
error,
|
||||||
shortUrlsList: PropTypes.arrayOf(shortUrlType),
|
shortUrlsList,
|
||||||
selectedServer: serverType,
|
selectedServer,
|
||||||
createNewVisit: PropTypes.func,
|
createNewVisit,
|
||||||
mercureInfo: MercureInfoType,
|
mercureInfo,
|
||||||
};
|
}) => {
|
||||||
|
const { orderBy } = shortUrlsListParams;
|
||||||
refreshList = (extraParams) => {
|
const [ order, setOrder ] = useState({
|
||||||
const { listShortUrls, shortUrlsListParams } = this.props;
|
orderField: orderBy && head(keys(orderBy)),
|
||||||
|
orderDir: orderBy && head(values(orderBy)),
|
||||||
listShortUrls({
|
|
||||||
...shortUrlsListParams,
|
|
||||||
...extraParams,
|
|
||||||
});
|
});
|
||||||
};
|
const refreshList = (extraParams) => listShortUrls({ ...shortUrlsListParams, ...extraParams });
|
||||||
|
const handleOrderBy = (orderField, orderDir) => {
|
||||||
handleOrderBy = (orderField, orderDir) => {
|
setOrder({ orderField, orderDir });
|
||||||
this.setState({ orderField, orderDir });
|
refreshList({ orderBy: { [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 (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={this.state.orderDir === 'ASC' ? caretUpIcon : caretDownIcon}
|
|
||||||
className="short-urls-list__header-icon"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const { orderBy } = props.shortUrlsListParams;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
orderField: orderBy ? head(keys(orderBy)) : undefined,
|
|
||||||
orderDir: orderBy ? head(values(orderBy)) : undefined,
|
|
||||||
};
|
};
|
||||||
}
|
const orderByColumn = (columnName) => () =>
|
||||||
|
handleOrderBy(columnName, determineOrderDir(columnName, order.orderField, order.orderDir));
|
||||||
|
const renderOrderIcon = (field) => {
|
||||||
|
if (order.orderField !== field) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
if (!order.orderDir) {
|
||||||
const { match: { params }, location, shortUrlsListParams } = this.props;
|
return null;
|
||||||
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
|
}
|
||||||
const tags = query.tag ? [ query.tag ] : shortUrlsListParams.tags;
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<tr>
|
<FontAwesomeIcon
|
||||||
<td colSpan="6" className="text-center table-danger">Something went wrong while loading short URLs :(</td>
|
icon={order.orderDir === 'ASC' ? caretUpIcon : caretDownIcon}
|
||||||
</tr>
|
className="short-urls-list__header-icon"
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
const renderShortUrls = () => {
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="6" className="text-center table-danger">Something went wrong while loading short URLs :(</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <tr><td colSpan="6" className="text-center">Loading...</td></tr>;
|
return <tr><td colSpan="6" className="text-center">Loading...</td></tr>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loading && isEmpty(shortUrlsList)) {
|
if (!loading && isEmpty(shortUrlsList)) {
|
||||||
return <tr><td colSpan="6" className="text-center">No results found</td></tr>;
|
return <tr><td colSpan="6" className="text-center">No results found</td></tr>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return shortUrlsList.map((shortUrl) => (
|
return shortUrlsList.map((shortUrl) => (
|
||||||
<ShortUrlsRow
|
<ShortUrlsRow
|
||||||
key={shortUrl.shortUrl}
|
key={shortUrl.shortUrl}
|
||||||
shortUrl={shortUrl}
|
shortUrl={shortUrl}
|
||||||
selectedServer={selectedServer}
|
selectedServer={selectedServer}
|
||||||
refreshList={this.refreshList}
|
refreshList={refreshList}
|
||||||
shortUrlsListParams={shortUrlsListParams}
|
shortUrlsListParams={shortUrlsListParams}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="d-block d-md-none mb-3">
|
<div className="d-block d-md-none mb-3">
|
||||||
<SortingDropdown
|
<SortingDropdown
|
||||||
items={SORTABLE_FIELDS}
|
items={SORTABLE_FIELDS}
|
||||||
orderField={this.state.orderField}
|
orderField={order.orderField}
|
||||||
orderDir={this.state.orderDir}
|
orderDir={order.orderDir}
|
||||||
onChange={this.handleOrderBy}
|
onChange={handleOrderBy}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<table className="table table-striped table-hover">
|
<table className="table table-striped table-hover">
|
||||||
|
@ -169,42 +151,46 @@ const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Compon
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
|
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
|
||||||
onClick={this.orderByColumn('dateCreated')}
|
onClick={orderByColumn('dateCreated')}
|
||||||
>
|
>
|
||||||
{this.renderOrderIcon('dateCreated')}
|
{renderOrderIcon('dateCreated')}
|
||||||
Created at
|
Created at
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
|
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
|
||||||
onClick={this.orderByColumn('shortCode')}
|
onClick={orderByColumn('shortCode')}
|
||||||
>
|
>
|
||||||
{this.renderOrderIcon('shortCode')}
|
{renderOrderIcon('shortCode')}
|
||||||
Short URL
|
Short URL
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
|
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
|
||||||
onClick={this.orderByColumn('longUrl')}
|
onClick={orderByColumn('longUrl')}
|
||||||
>
|
>
|
||||||
{this.renderOrderIcon('longUrl')}
|
{renderOrderIcon('longUrl')}
|
||||||
Long URL
|
Long URL
|
||||||
</th>
|
</th>
|
||||||
<th className="short-urls-list__header-cell">Tags</th>
|
<th className="short-urls-list__header-cell">Tags</th>
|
||||||
<th
|
<th
|
||||||
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
|
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
|
||||||
onClick={this.orderByColumn('visits')}
|
onClick={orderByColumn('visits')}
|
||||||
>
|
>
|
||||||
<span className="indivisible">{this.renderOrderIcon('visits')} Visits</span>
|
<span className="indivisible">{renderOrderIcon('visits')} Visits</span>
|
||||||
</th>
|
</th>
|
||||||
<th className="short-urls-list__header-cell"> </th>
|
<th className="short-urls-list__header-cell"> </th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{this.renderShortUrls()}
|
{renderShortUrls()}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
ShortUrlsListComp.propTypes = propTypes;
|
||||||
|
|
||||||
|
return ShortUrlsListComp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ShortUrlsList;
|
export default ShortUrlsList;
|
||||||
|
|
|
@ -71,10 +71,6 @@ describe('<ShortUrlsList />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render 6 table header cells with conditional order by icon', () => {
|
it('should render 6 table header cells with conditional order by icon', () => {
|
||||||
const orderDirOptionToIconMap = {
|
|
||||||
ASC: caretUpIcon,
|
|
||||||
DESC: caretDownIcon,
|
|
||||||
};
|
|
||||||
const getThElementForSortableField = (sortableField) => wrapper.find('table')
|
const getThElementForSortableField = (sortableField) => wrapper.find('table')
|
||||||
.find('thead')
|
.find('thead')
|
||||||
.find('tr')
|
.find('tr')
|
||||||
|
@ -82,24 +78,18 @@ describe('<ShortUrlsList />', () => {
|
||||||
.filterWhere((e) => e.text().includes(SORTABLE_FIELDS[sortableField]));
|
.filterWhere((e) => e.text().includes(SORTABLE_FIELDS[sortableField]));
|
||||||
|
|
||||||
Object.keys(SORTABLE_FIELDS).forEach((sortableField) => {
|
Object.keys(SORTABLE_FIELDS).forEach((sortableField) => {
|
||||||
const sortableThElementWrapper = getThElementForSortableField(sortableField);
|
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(0);
|
||||||
|
|
||||||
expect(sortableThElementWrapper.find(FontAwesomeIcon)).toHaveLength(0);
|
getThElementForSortableField(sortableField).simulate('click');
|
||||||
|
|
||||||
sortableThElementWrapper.simulate('click');
|
|
||||||
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(1);
|
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(1);
|
||||||
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon).prop('icon')).toEqual(
|
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon).prop('icon')).toEqual(caretUpIcon);
|
||||||
orderDirOptionToIconMap.ASC,
|
|
||||||
);
|
|
||||||
|
|
||||||
sortableThElementWrapper.simulate('click');
|
getThElementForSortableField(sortableField).simulate('click');
|
||||||
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(1);
|
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(1);
|
||||||
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon).prop('icon')).toEqual(
|
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon).prop('icon')).toEqual(caretDownIcon);
|
||||||
orderDirOptionToIconMap.DESC,
|
|
||||||
);
|
|
||||||
|
|
||||||
sortableThElementWrapper.simulate('click');
|
getThElementForSortableField(sortableField).simulate('click');
|
||||||
expect(sortableThElementWrapper.find(FontAwesomeIcon)).toHaveLength(0);
|
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue