mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-21 06:53:50 +00:00
Extracted short URLs table into reusable component to use both on list section and overview section
This commit is contained in:
8
src/servers/Overview.scss
Normal file
8
src/servers/Overview.scss
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.overview__card.overview__card {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview__card-title {
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
@@ -1,50 +1,77 @@
|
|||||||
import { useEffect } from 'react';
|
import { FC, useEffect } from 'react';
|
||||||
import { Card, CardText, CardTitle } from 'reactstrap';
|
import { Card, CardBody, CardHeader, CardText, CardTitle } from 'reactstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { ShortUrlsListParams } from '../short-urls/reducers/shortUrlsListParams';
|
import { ShortUrlsListParams } from '../short-urls/reducers/shortUrlsListParams';
|
||||||
import { ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList';
|
import { ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList';
|
||||||
import { prettify } from '../utils/helpers/numbers';
|
import { prettify } from '../utils/helpers/numbers';
|
||||||
import { TagsList } from '../tags/reducers/tagsList';
|
import { TagsList } from '../tags/reducers/tagsList';
|
||||||
|
import { ShortUrlsTableProps } from '../short-urls/ShortUrlsTable';
|
||||||
|
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
|
import { isServerWithId, SelectedServer } from './data';
|
||||||
|
import './Overview.scss';
|
||||||
|
|
||||||
interface OverviewConnectProps {
|
interface OverviewConnectProps {
|
||||||
shortUrlsList: ShortUrlsListState;
|
shortUrlsList: ShortUrlsListState;
|
||||||
listShortUrls: (params: ShortUrlsListParams) => void;
|
listShortUrls: (params: ShortUrlsListParams) => void;
|
||||||
listTags: Function;
|
listTags: Function;
|
||||||
tagsList: TagsList;
|
tagsList: TagsList;
|
||||||
|
selectedServer: SelectedServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Overview = ({ shortUrlsList, listShortUrls, listTags, tagsList }: OverviewConnectProps) => {
|
export const Overview = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercureHub((
|
||||||
|
{ shortUrlsList, listShortUrls, listTags, tagsList, selectedServer }: OverviewConnectProps,
|
||||||
|
) => {
|
||||||
const { loading, error, shortUrls } = shortUrlsList;
|
const { loading, error, shortUrls } = shortUrlsList;
|
||||||
const { loading: loadingTags } = tagsList;
|
const { loading: loadingTags } = tagsList;
|
||||||
|
const serverId = !isServerWithId(selectedServer) ? '' : selectedServer.id;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listShortUrls({ itemsPerPage: 5 });
|
listShortUrls({ itemsPerPage: 5, orderBy: { dateCreated: 'DESC' } });
|
||||||
listTags();
|
listTags();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<>
|
||||||
<div className="col-sm-4">
|
<div className="row mb-3">
|
||||||
<Card className="text-center mb-2 mb-sm-0" body>
|
<div className="col-sm-4">
|
||||||
<CardTitle tag="h5">Visits</CardTitle>
|
<Card className="overview__card mb-2" body>
|
||||||
<CardText tag="h2">?</CardText>
|
<CardTitle tag="h5" className="overview__card-title">Visits</CardTitle>
|
||||||
</Card>
|
<CardText tag="h2">?</CardText>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-4">
|
||||||
|
<Card className="overview__card mb-2" body>
|
||||||
|
<CardTitle tag="h5" className="overview__card-title">Short URLs</CardTitle>
|
||||||
|
<CardText tag="h2">
|
||||||
|
{loading && !error && 'Loading...'}
|
||||||
|
{error && !loading && 'Failed :('}
|
||||||
|
{!error && !loading && prettify(shortUrls?.pagination.totalItems ?? 0)}
|
||||||
|
</CardText>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-4">
|
||||||
|
<Card className="overview__card mb-2" body>
|
||||||
|
<CardTitle tag="h5" className="overview__card-title">Tags</CardTitle>
|
||||||
|
<CardText tag="h2">{loadingTags ? 'Loading... ' : prettify(tagsList.tags.length)}</CardText>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-sm-4">
|
<Card className="mb-4">
|
||||||
<Card className="text-center mb-2 mb-sm-0" body>
|
<CardHeader>
|
||||||
<CardTitle tag="h5">Short URLs</CardTitle>
|
Create short URL
|
||||||
<CardText tag="h2">
|
<Link className="float-right" to={`/server/${serverId}/create-short-url`}>More options »</Link>
|
||||||
{loading && !error && 'Loading...'}
|
</CardHeader>
|
||||||
{error && !loading && 'Failed :('}
|
<CardBody>Create</CardBody>
|
||||||
{!error && !loading && prettify(shortUrls?.pagination.totalItems ?? 0)}
|
</Card>
|
||||||
</CardText>
|
<Card>
|
||||||
</Card>
|
<CardHeader>
|
||||||
</div>
|
Recently created URLs
|
||||||
<div className="col-sm-4">
|
<Link className="float-right" to={`/server/${serverId}/list-short-urls/1`}>See all »</Link>
|
||||||
<Card className="text-center" body>
|
</CardHeader>
|
||||||
<CardTitle tag="h5">Tags</CardTitle>
|
<CardBody>
|
||||||
<CardText tag="h2">{loadingTags ? 'Loading... ' : prettify(tagsList.tags.length)}</CardText>
|
<ShortUrlsTable shortUrlsList={shortUrlsList} selectedServer={selectedServer} className="mb-0" />
|
||||||
</Card>
|
</CardBody>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}, () => 'https://shlink.io/new-visit');
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
|
|||||||
bottle.serviceFactory('ServerError', ServerError, 'DeleteServerButton');
|
bottle.serviceFactory('ServerError', ServerError, 'DeleteServerButton');
|
||||||
bottle.decorator('ServerError', connect([ 'servers', 'selectedServer' ]));
|
bottle.decorator('ServerError', connect([ 'servers', 'selectedServer' ]));
|
||||||
|
|
||||||
bottle.serviceFactory('Overview', () => Overview);
|
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable');
|
||||||
bottle.decorator('Overview', connect(
|
bottle.decorator('Overview', connect(
|
||||||
[ 'shortUrlsList', 'tagsList' ],
|
[ 'shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo' ],
|
||||||
[ 'listShortUrls', 'listTags' ],
|
[ 'listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo' ],
|
||||||
));
|
));
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import { FC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import { ShlinkShortUrlsResponse } from '../utils/services/types';
|
|
||||||
import Paginator from './Paginator';
|
import Paginator from './Paginator';
|
||||||
import { ShortUrlsListProps, WithList } from './ShortUrlsList';
|
import { ShortUrlsListProps } from './ShortUrlsList';
|
||||||
|
|
||||||
interface ShortUrlsProps extends ShortUrlsListProps {
|
const ShortUrls = (SearchBar: FC, ShortUrlsList: FC<ShortUrlsListProps>) => (props: ShortUrlsListProps) => {
|
||||||
shortUrlsList?: ShlinkShortUrlsResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ShortUrls = (SearchBar: FC, ShortUrlsList: FC<ShortUrlsListProps & WithList>) => (props: ShortUrlsProps) => {
|
|
||||||
const { match, shortUrlsList } = props;
|
const { match, shortUrlsList } = props;
|
||||||
const { page = '1', serverId = '' } = match?.params ?? {};
|
const { page = '1', serverId = '' } = match?.params ?? {};
|
||||||
const { data = [], pagination } = shortUrlsList ?? {};
|
const { pagination } = shortUrlsList?.shortUrls ?? {};
|
||||||
const [ urlsListKey, setUrlsListKey ] = useState(`${serverId}_${page}`);
|
const [ urlsListKey, setUrlsListKey ] = useState(`${serverId}_${page}`);
|
||||||
|
|
||||||
// Using a key on a component makes react to create a new instance every time the key changes
|
// Using a key on a component makes react to create a new instance every time the key changes
|
||||||
@@ -23,7 +18,7 @@ const ShortUrls = (SearchBar: FC, ShortUrlsList: FC<ShortUrlsListProps & WithLis
|
|||||||
<>
|
<>
|
||||||
<div className="form-group"><SearchBar /></div>
|
<div className="form-group"><SearchBar /></div>
|
||||||
<div>
|
<div>
|
||||||
<ShortUrlsList {...props} shortUrlsList={data} key={urlsListKey} />
|
<ShortUrlsList {...props} key={urlsListKey} />
|
||||||
<Paginator paginator={pagination} serverId={serverId} />
|
<Paginator paginator={pagination} serverId={serverId} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,19 +1,3 @@
|
|||||||
@import '../utils/base';
|
|
||||||
|
|
||||||
.short-urls-list__header {
|
|
||||||
@media (max-width: $smMax) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.short-urls-list__header--with-action {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.short-urls-list__header-icon {
|
.short-urls-list__header-icon {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.short-urls-list__header-cell--with-action {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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, keys, values } from 'ramda';
|
||||||
import { FC, useEffect, useState } from 'react';
|
import { FC, useEffect, useState } from 'react';
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
import { RouteComponentProps } from 'react-router';
|
import { RouteComponentProps } from 'react-router';
|
||||||
@@ -9,9 +9,8 @@ import { determineOrderDir, OrderDir } from '../utils/utils';
|
|||||||
import { SelectedServer } from '../servers/data';
|
import { SelectedServer } from '../servers/data';
|
||||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
||||||
import { ShortUrlsRowProps } from './helpers/ShortUrlsRow';
|
|
||||||
import { ShortUrl } from './data';
|
|
||||||
import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams';
|
import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams';
|
||||||
|
import { ShortUrlsTableProps } from './ShortUrlsTable';
|
||||||
import './ShortUrlsList.scss';
|
import './ShortUrlsList.scss';
|
||||||
|
|
||||||
interface RouteParams {
|
interface RouteParams {
|
||||||
@@ -19,28 +18,23 @@ interface RouteParams {
|
|||||||
serverId: string;
|
serverId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WithList {
|
export interface ShortUrlsListProps extends RouteComponentProps<RouteParams> {
|
||||||
shortUrlsList: ShortUrl[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ShortUrlsListProps extends ShortUrlsListState, RouteComponentProps<RouteParams> {
|
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
|
shortUrlsList: ShortUrlsListState;
|
||||||
listShortUrls: (params: ShortUrlsListParams) => void;
|
listShortUrls: (params: ShortUrlsListParams) => void;
|
||||||
shortUrlsListParams: ShortUrlsListParams;
|
shortUrlsListParams: ShortUrlsListParams;
|
||||||
resetShortUrlParams: () => void;
|
resetShortUrlParams: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShortUrlsList = (ShortUrlsRow: FC<ShortUrlsRowProps>) => boundToMercureHub(({
|
const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>) => boundToMercureHub(({
|
||||||
listShortUrls,
|
listShortUrls,
|
||||||
resetShortUrlParams,
|
resetShortUrlParams,
|
||||||
shortUrlsListParams,
|
shortUrlsListParams,
|
||||||
match,
|
match,
|
||||||
location,
|
location,
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
shortUrlsList,
|
shortUrlsList,
|
||||||
selectedServer,
|
selectedServer,
|
||||||
}: ShortUrlsListProps & WithList) => {
|
}: ShortUrlsListProps) => {
|
||||||
const { orderBy } = shortUrlsListParams;
|
const { orderBy } = shortUrlsListParams;
|
||||||
const [ order, setOrder ] = useState<{ orderField?: OrderableFields; orderDir?: OrderDir }>({
|
const [ order, setOrder ] = useState<{ orderField?: OrderableFields; orderDir?: OrderDir }>({
|
||||||
orderField: orderBy && (head(keys(orderBy)) as OrderableFields),
|
orderField: orderBy && (head(keys(orderBy)) as OrderableFields),
|
||||||
@@ -69,39 +63,12 @@ const ShortUrlsList = (ShortUrlsRow: FC<ShortUrlsRowProps>) => boundToMercureHub
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
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) {
|
|
||||||
return <tr><td colSpan={6} className="text-center">Loading...</td></tr>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!loading && isEmpty(shortUrlsList)) {
|
|
||||||
return <tr><td colSpan={6} className="text-center">No results found</td></tr>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return shortUrlsList.map((shortUrl) => (
|
|
||||||
<ShortUrlsRow
|
|
||||||
key={shortUrl.shortUrl}
|
|
||||||
shortUrl={shortUrl}
|
|
||||||
selectedServer={selectedServer}
|
|
||||||
refreshList={refreshList}
|
|
||||||
shortUrlsListParams={shortUrlsListParams}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
|
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
|
||||||
const tags = query.tag ? [ query.tag as string ] : shortUrlsListParams.tags;
|
const tags = query.tag ? [ query.tag as string ] : shortUrlsListParams.tags;
|
||||||
|
|
||||||
refreshList({ page: match.params.page, tags });
|
refreshList({ page: match.params.page, tags, itemsPerPage: undefined });
|
||||||
|
|
||||||
return resetShortUrlParams;
|
return resetShortUrlParams;
|
||||||
}, []);
|
}, []);
|
||||||
@@ -116,44 +83,14 @@ const ShortUrlsList = (ShortUrlsRow: FC<ShortUrlsRowProps>) => boundToMercureHub
|
|||||||
onChange={handleOrderBy}
|
onChange={handleOrderBy}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<table className="table table-striped table-hover">
|
<ShortUrlsTable
|
||||||
<thead className="short-urls-list__header">
|
orderByColumn={orderByColumn}
|
||||||
<tr>
|
renderOrderIcon={renderOrderIcon}
|
||||||
<th
|
selectedServer={selectedServer}
|
||||||
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
|
refreshList={refreshList}
|
||||||
onClick={orderByColumn('dateCreated')}
|
shortUrlsListParams={shortUrlsListParams}
|
||||||
>
|
shortUrlsList={shortUrlsList}
|
||||||
{renderOrderIcon('dateCreated')}
|
/>
|
||||||
Created at
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
|
|
||||||
onClick={orderByColumn('shortCode')}
|
|
||||||
>
|
|
||||||
{renderOrderIcon('shortCode')}
|
|
||||||
Short URL
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
|
|
||||||
onClick={orderByColumn('longUrl')}
|
|
||||||
>
|
|
||||||
{renderOrderIcon('longUrl')}
|
|
||||||
Long URL
|
|
||||||
</th>
|
|
||||||
<th className="short-urls-list__header-cell">Tags</th>
|
|
||||||
<th
|
|
||||||
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
|
|
||||||
onClick={orderByColumn('visits')}
|
|
||||||
>
|
|
||||||
<span className="indivisible">{renderOrderIcon('visits')} Visits</span>
|
|
||||||
</th>
|
|
||||||
<th className="short-urls-list__header-cell"> </th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{renderShortUrls()}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, () => 'https://shlink.io/new-visit');
|
}, () => 'https://shlink.io/new-visit');
|
||||||
|
|||||||
11
src/short-urls/ShortUrlsTable.scss
Normal file
11
src/short-urls/ShortUrlsTable.scss
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
@import '../utils/base';
|
||||||
|
|
||||||
|
.short-urls-table__header {
|
||||||
|
@media (max-width: $smMax) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.short-urls-table__header-cell--with-action {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
91
src/short-urls/ShortUrlsTable.tsx
Normal file
91
src/short-urls/ShortUrlsTable.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { FC, ReactNode } from 'react';
|
||||||
|
import { isEmpty } from 'ramda';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { SelectedServer } from '../servers/data';
|
||||||
|
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
||||||
|
import { ShortUrlsRowProps } from './helpers/ShortUrlsRow';
|
||||||
|
import { OrderableFields, ShortUrlsListParams } from './reducers/shortUrlsListParams';
|
||||||
|
import './ShortUrlsTable.scss';
|
||||||
|
|
||||||
|
export interface ShortUrlsTableProps {
|
||||||
|
orderByColumn?: (column: OrderableFields) => () => void;
|
||||||
|
renderOrderIcon?: (column: OrderableFields) => ReactNode;
|
||||||
|
shortUrlsList: ShortUrlsListState;
|
||||||
|
selectedServer: SelectedServer;
|
||||||
|
refreshList?: Function;
|
||||||
|
shortUrlsListParams?: ShortUrlsListParams;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShortUrlsTable = (ShortUrlsRow: FC<ShortUrlsRowProps>) => ({
|
||||||
|
orderByColumn,
|
||||||
|
renderOrderIcon,
|
||||||
|
shortUrlsList,
|
||||||
|
refreshList,
|
||||||
|
shortUrlsListParams,
|
||||||
|
selectedServer,
|
||||||
|
className,
|
||||||
|
}: ShortUrlsTableProps) => {
|
||||||
|
const { error, loading, shortUrls } = shortUrlsList;
|
||||||
|
const orderableColumnsClasses = classNames('short-urls-table__header-cell', {
|
||||||
|
'short-urls-table__header-cell--with-action': !!orderByColumn,
|
||||||
|
});
|
||||||
|
const tableClasses = classNames('table table-striped table-hover', className);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return <tr><td colSpan={6} className="text-center">Loading...</td></tr>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loading && isEmpty(shortUrlsList)) {
|
||||||
|
return <tr><td colSpan={6} className="text-center">No results found</td></tr>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortUrls?.data.map((shortUrl) => (
|
||||||
|
<ShortUrlsRow
|
||||||
|
key={shortUrl.shortUrl}
|
||||||
|
shortUrl={shortUrl}
|
||||||
|
selectedServer={selectedServer}
|
||||||
|
refreshList={refreshList}
|
||||||
|
shortUrlsListParams={shortUrlsListParams}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className={tableClasses}>
|
||||||
|
<thead className="short-urls-table__header">
|
||||||
|
<tr>
|
||||||
|
<th className={orderableColumnsClasses} onClick={orderByColumn?.('dateCreated')}>
|
||||||
|
{renderOrderIcon?.('dateCreated')}
|
||||||
|
Created at
|
||||||
|
</th>
|
||||||
|
<th className={orderableColumnsClasses} onClick={orderByColumn?.('shortCode')}>
|
||||||
|
{renderOrderIcon?.('shortCode')}
|
||||||
|
Short URL
|
||||||
|
</th>
|
||||||
|
<th className={orderableColumnsClasses} onClick={orderByColumn?.('longUrl')}>
|
||||||
|
{renderOrderIcon?.('longUrl')}
|
||||||
|
Long URL
|
||||||
|
</th>
|
||||||
|
<th className="short-urls-table__header-cell">Tags</th>
|
||||||
|
<th className={orderableColumnsClasses} onClick={orderByColumn?.('visits')}>
|
||||||
|
<span className="indivisible">{renderOrderIcon?.('visits')} Visits</span>
|
||||||
|
</th>
|
||||||
|
<th className="short-urls-table__header-cell"> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{renderShortUrls()}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -16,8 +16,8 @@ import { ShortUrlsRowMenuProps } from './ShortUrlsRowMenu';
|
|||||||
import './ShortUrlsRow.scss';
|
import './ShortUrlsRow.scss';
|
||||||
|
|
||||||
export interface ShortUrlsRowProps {
|
export interface ShortUrlsRowProps {
|
||||||
refreshList: Function;
|
refreshList?: Function;
|
||||||
shortUrlsListParams: ShortUrlsListParams;
|
shortUrlsListParams?: ShortUrlsListParams;
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
shortUrl: ShortUrl;
|
shortUrl: ShortUrl;
|
||||||
}
|
}
|
||||||
@@ -36,14 +36,14 @@ const ShortUrlsRow = (
|
|||||||
return <i className="indivisible"><small>No tags</small></i>;
|
return <i className="indivisible"><small>No tags</small></i>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedTags = shortUrlsListParams.tags ?? [];
|
const selectedTags = shortUrlsListParams?.tags ?? [];
|
||||||
|
|
||||||
return tags.map((tag) => (
|
return tags.map((tag) => (
|
||||||
<Tag
|
<Tag
|
||||||
colorGenerator={colorGenerator}
|
colorGenerator={colorGenerator}
|
||||||
key={tag}
|
key={tag}
|
||||||
text={tag}
|
text={tag}
|
||||||
onClick={() => refreshList({ tags: [ ...selectedTags, tag ] })}
|
onClick={() => refreshList?.({ tags: [ ...selectedTags, tag ] })}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { connect as reduxConnect } from 'react-redux';
|
|
||||||
import { assoc } from 'ramda';
|
|
||||||
import Bottle from 'bottlejs';
|
import Bottle from 'bottlejs';
|
||||||
import ShortUrls from '../ShortUrls';
|
import ShortUrls from '../ShortUrls';
|
||||||
import SearchBar from '../SearchBar';
|
import SearchBar from '../SearchBar';
|
||||||
@@ -19,27 +17,26 @@ import { editShortUrlTags, resetShortUrlsTags } from '../reducers/shortUrlTags';
|
|||||||
import { editShortUrlMeta, resetShortUrlMeta } from '../reducers/shortUrlMeta';
|
import { editShortUrlMeta, resetShortUrlMeta } from '../reducers/shortUrlMeta';
|
||||||
import { resetShortUrlParams } from '../reducers/shortUrlsListParams';
|
import { resetShortUrlParams } from '../reducers/shortUrlsListParams';
|
||||||
import { editShortUrl } from '../reducers/shortUrlEdition';
|
import { editShortUrl } from '../reducers/shortUrlEdition';
|
||||||
import { ConnectDecorator, ShlinkState } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
|
import { ShortUrlsTable } from '../ShortUrlsTable';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList');
|
bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList');
|
||||||
bottle.decorator('ShortUrls', reduxConnect(
|
bottle.decorator('ShortUrls', connect([ 'shortUrlsList' ]));
|
||||||
(state: ShlinkState) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList),
|
|
||||||
));
|
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator', 'ForServerVersion');
|
bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator', 'ForServerVersion');
|
||||||
bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], [ 'listShortUrls' ]));
|
bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], [ 'listShortUrls' ]));
|
||||||
|
|
||||||
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow');
|
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable');
|
||||||
bottle.decorator('ShortUrlsList', connect(
|
bottle.decorator('ShortUrlsList', connect(
|
||||||
[ 'selectedServer', 'shortUrlsListParams', 'mercureInfo' ],
|
[ 'selectedServer', 'shortUrlsListParams', 'mercureInfo' ],
|
||||||
[ 'listShortUrls', 'resetShortUrlParams', 'createNewVisits', 'loadMercureInfo' ],
|
[ 'listShortUrls', 'resetShortUrlParams', 'createNewVisits', 'loadMercureInfo' ],
|
||||||
));
|
));
|
||||||
|
|
||||||
|
bottle.serviceFactory('ShortUrlsTable', ShortUrlsTable, 'ShortUrlsRow');
|
||||||
bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'useStateFlagTimeout');
|
bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator', 'useStateFlagTimeout');
|
||||||
|
|
||||||
bottle.serviceFactory(
|
bottle.serviceFactory(
|
||||||
'ShortUrlsRowMenu',
|
'ShortUrlsRowMenu',
|
||||||
ShortUrlsRowMenu,
|
ShortUrlsRowMenu,
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faCaretDown as caretDownIcon, faCaretUp as caretUpIcon } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import shortUrlsListCreator, { ShortUrlsListProps } from '../../src/short-urls/ShortUrlsList';
|
import shortUrlsListCreator, { ShortUrlsListProps } from '../../src/short-urls/ShortUrlsList';
|
||||||
import { ShortUrl } from '../../src/short-urls/data';
|
import { ShortUrl } from '../../src/short-urls/data';
|
||||||
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||||
import { SORTABLE_FIELDS } from '../../src/short-urls/reducers/shortUrlsListParams';
|
import { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList';
|
||||||
|
import SortingDropdown from '../../src/utils/SortingDropdown';
|
||||||
|
|
||||||
describe('<ShortUrlsList />', () => {
|
describe('<ShortUrlsList />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const ShortUrlsRow = () => null;
|
const ShortUrlsTable = () => null;
|
||||||
const listShortUrlsMock = jest.fn();
|
const listShortUrlsMock = jest.fn();
|
||||||
const resetShortUrlParamsMock = jest.fn();
|
const resetShortUrlParamsMock = jest.fn();
|
||||||
|
const shortUrlsList = Mock.of<ShortUrlsListModel>({
|
||||||
|
shortUrls: {
|
||||||
|
data: [
|
||||||
|
Mock.of<ShortUrl>({
|
||||||
|
shortCode: 'testShortCode',
|
||||||
|
shortUrl: 'https://www.example.com/testShortUrl',
|
||||||
|
longUrl: 'https://www.example.com/testLongUrl',
|
||||||
|
tags: [ 'test tag' ],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const ShortUrlsList = shortUrlsListCreator(ShortUrlsRow);
|
const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
@@ -29,18 +40,7 @@ describe('<ShortUrlsList />', () => {
|
|||||||
}}
|
}}
|
||||||
match={{ params: {} } as any}
|
match={{ params: {} } as any}
|
||||||
location={{} as any}
|
location={{} as any}
|
||||||
loading={false}
|
shortUrlsList={shortUrlsList}
|
||||||
error={false}
|
|
||||||
shortUrlsList={
|
|
||||||
[
|
|
||||||
Mock.of<ShortUrl>({
|
|
||||||
shortCode: 'testShortCode',
|
|
||||||
shortUrl: 'https://www.example.com/testShortUrl',
|
|
||||||
longUrl: 'https://www.example.com/testLongUrl',
|
|
||||||
tags: [ 'test tag' ],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
/>,
|
/>,
|
||||||
).dive(); // Dive is needed as this component is wrapped in a HOC
|
).dive(); // Dive is needed as this component is wrapped in a HOC
|
||||||
});
|
});
|
||||||
@@ -48,50 +48,11 @@ describe('<ShortUrlsList />', () => {
|
|||||||
afterEach(jest.resetAllMocks);
|
afterEach(jest.resetAllMocks);
|
||||||
afterEach(() => wrapper?.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
it('wraps a ShortUrlsList with 1 ShortUrlsRow', () => {
|
it('wraps a ShortUrlsTable', () => {
|
||||||
expect(wrapper.find(ShortUrlsRow)).toHaveLength(1);
|
expect(wrapper.find(ShortUrlsTable)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render inner table by default', () => {
|
it('wraps a SortingDropdown', () => {
|
||||||
expect(wrapper.find('table')).toHaveLength(1);
|
expect(wrapper.find(SortingDropdown)).toHaveLength(1);
|
||||||
});
|
|
||||||
|
|
||||||
it('should render table header by default', () => {
|
|
||||||
expect(wrapper.find('table').find('thead')).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render 6 table header cells by default', () => {
|
|
||||||
expect(wrapper.find('table').find('thead').find('tr').find('th')).toHaveLength(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render 6 table header cells without order by icon by default', () => {
|
|
||||||
const thElements = wrapper.find('table').find('thead').find('tr').find('th');
|
|
||||||
|
|
||||||
thElements.forEach((thElement) => {
|
|
||||||
expect(thElement.find(FontAwesomeIcon)).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render 6 table header cells with conditional order by icon', () => {
|
|
||||||
const getThElementForSortableField = (sortableField: string) => wrapper.find('table')
|
|
||||||
.find('thead')
|
|
||||||
.find('tr')
|
|
||||||
.find('th')
|
|
||||||
.filterWhere((e) => e.text().includes(SORTABLE_FIELDS[sortableField as keyof typeof SORTABLE_FIELDS]));
|
|
||||||
|
|
||||||
Object.keys(SORTABLE_FIELDS).forEach((sortableField) => {
|
|
||||||
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(0);
|
|
||||||
|
|
||||||
getThElementForSortableField(sortableField).simulate('click');
|
|
||||||
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(1);
|
|
||||||
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon).prop('icon')).toEqual(caretUpIcon);
|
|
||||||
|
|
||||||
getThElementForSortableField(sortableField).simulate('click');
|
|
||||||
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(1);
|
|
||||||
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon).prop('icon')).toEqual(caretDownIcon);
|
|
||||||
|
|
||||||
getThElementForSortableField(sortableField).simulate('click');
|
|
||||||
expect(getThElementForSortableField(sortableField).find(FontAwesomeIcon)).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
59
test/short-urls/ShortUrlsTable.test.tsx
Normal file
59
test/short-urls/ShortUrlsTable.test.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { ShortUrlsTable as shortUrlsTableCreator } from '../../src/short-urls/ShortUrlsTable';
|
||||||
|
import { SORTABLE_FIELDS } from '../../src/short-urls/reducers/shortUrlsListParams';
|
||||||
|
import { ShortUrlsList } from '../../src/short-urls/reducers/shortUrlsList';
|
||||||
|
|
||||||
|
describe('<ShortUrlsTable />', () => {
|
||||||
|
let wrapper: ShallowWrapper;
|
||||||
|
const shortUrlsList = Mock.all<ShortUrlsList>();
|
||||||
|
const orderByColumn = jest.fn();
|
||||||
|
const ShortUrlsRow = () => null;
|
||||||
|
|
||||||
|
const ShortUrlsTable = shortUrlsTableCreator(ShortUrlsRow);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = shallow(
|
||||||
|
<ShortUrlsTable shortUrlsList={shortUrlsList} selectedServer={null} orderByColumn={() => orderByColumn} />,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(jest.resetAllMocks);
|
||||||
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
|
it('should render inner table by default', () => {
|
||||||
|
expect(wrapper.find('table')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render table header by default', () => {
|
||||||
|
expect(wrapper.find('table').find('thead')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render 6 table header cells by default', () => {
|
||||||
|
expect(wrapper.find('table').find('thead').find('tr').find('th')).toHaveLength(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render 6 table header cells without order by icon by default', () => {
|
||||||
|
const thElements = wrapper.find('table').find('thead').find('tr').find('th');
|
||||||
|
|
||||||
|
thElements.forEach((thElement) => {
|
||||||
|
expect(thElement.find(FontAwesomeIcon)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render 6 table header cells with conditional order by icon', () => {
|
||||||
|
const getThElementForSortableField = (sortableField: string) => wrapper.find('table')
|
||||||
|
.find('thead')
|
||||||
|
.find('tr')
|
||||||
|
.find('th')
|
||||||
|
.filterWhere((e) => e.text().includes(SORTABLE_FIELDS[sortableField as keyof typeof SORTABLE_FIELDS]));
|
||||||
|
const sortableFields = Object.keys(SORTABLE_FIELDS);
|
||||||
|
|
||||||
|
expect.assertions(sortableFields.length);
|
||||||
|
sortableFields.forEach((sortableField) => {
|
||||||
|
getThElementForSortableField(sortableField).simulate('click');
|
||||||
|
expect(orderByColumn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user