From 3c9c0fe994e8548fc56711a955cd2b152bb76221 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 5 Mar 2021 14:20:49 +0100 Subject: [PATCH 1/4] Added support for title field in short URL table --- src/short-urls/ShortUrlsList.scss | 2 +- src/short-urls/ShortUrlsTable.tsx | 35 +++++++++++++------ src/short-urls/data/index.ts | 1 + src/short-urls/helpers/ShortUrlsRow.scss | 4 --- src/short-urls/helpers/ShortUrlsRow.tsx | 9 +++-- .../reducers/shortUrlsListParams.ts | 1 + src/utils/helpers/features.ts | 7 ++++ test/short-urls/ShortUrlsTable.test.tsx | 35 ++++++++++++++----- 8 files changed, 68 insertions(+), 26 deletions(-) create mode 100644 src/utils/helpers/features.ts diff --git a/src/short-urls/ShortUrlsList.scss b/src/short-urls/ShortUrlsList.scss index f2a5edb2..5b1be099 100644 --- a/src/short-urls/ShortUrlsList.scss +++ b/src/short-urls/ShortUrlsList.scss @@ -1,3 +1,3 @@ .short-urls-list__header-icon { - margin-right: 5px; + margin-left: .4rem; } diff --git a/src/short-urls/ShortUrlsTable.tsx b/src/short-urls/ShortUrlsTable.tsx index 99e971a0..9bc0c9d6 100644 --- a/src/short-urls/ShortUrlsTable.tsx +++ b/src/short-urls/ShortUrlsTable.tsx @@ -2,6 +2,7 @@ import { FC, ReactNode } from 'react'; import { isEmpty } from 'ramda'; import classNames from 'classnames'; import { SelectedServer } from '../servers/data'; +import { titleIsSupported } from '../utils/helpers/features'; import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; import { ShortUrlsRowProps } from './helpers/ShortUrlsRow'; import { OrderableFields } from './reducers/shortUrlsListParams'; @@ -25,10 +26,10 @@ export const ShortUrlsTable = (ShortUrlsRow: FC) => ({ className, }: ShortUrlsTableProps) => { const { error, loading, shortUrls } = shortUrlsList; - const orderableColumnsClasses = classNames('short-urls-table__header-cell', { - 'short-urls-table__header-cell--with-action': !!orderByColumn, - }); + const actionableFieldClasses = classNames({ 'short-urls-table__header-cell--with-action': !!orderByColumn }); + const orderableColumnsClasses = classNames('short-urls-table__header-cell', actionableFieldClasses); const tableClasses = classNames('table table-hover', className); + const supportsTitle = titleIsSupported(selectedServer); const renderShortUrls = () => { if (error) { @@ -62,20 +63,34 @@ export const ShortUrlsTable = (ShortUrlsRow: FC) => ({ - {renderOrderIcon?.('dateCreated')} Created at + {renderOrderIcon?.('dateCreated')} - {renderOrderIcon?.('shortCode')} Short URL + {renderOrderIcon?.('shortCode')} - - {renderOrderIcon?.('longUrl')} - Long URL - + {!supportsTitle && ( + + Long URL + {renderOrderIcon?.('longUrl')} + + ) || ( + + + Title + {renderOrderIcon?.('title')} + +   /   + + Long URL + {renderOrderIcon?.('longUrl')} + + + )} Tags - {renderOrderIcon?.('visits')} Visits + Visits{renderOrderIcon?.('visits')}   diff --git a/src/short-urls/data/index.ts b/src/short-urls/data/index.ts index 237c6b6a..44c280bf 100644 --- a/src/short-urls/data/index.ts +++ b/src/short-urls/data/index.ts @@ -23,6 +23,7 @@ export interface ShortUrl { meta: Required>; tags: string[]; domain: string | null; + title?: string | null; } export interface ShortUrlMeta { diff --git a/src/short-urls/helpers/ShortUrlsRow.scss b/src/short-urls/helpers/ShortUrlsRow.scss index 17ec51ac..a199b69b 100644 --- a/src/short-urls/helpers/ShortUrlsRow.scss +++ b/src/short-urls/helpers/ShortUrlsRow.scss @@ -44,10 +44,6 @@ position: relative; } -.short-urls-row__cell--big { - transform: scale(1.5); -} - .short-urls-row__copy-hint { @include vertical-align(translateX(10px)); diff --git a/src/short-urls/helpers/ShortUrlsRow.tsx b/src/short-urls/helpers/ShortUrlsRow.tsx index 5551938c..db40e670 100644 --- a/src/short-urls/helpers/ShortUrlsRow.tsx +++ b/src/short-urls/helpers/ShortUrlsRow.tsx @@ -64,9 +64,14 @@ const ShortUrlsRow = ( - - + + {shortUrl.title ?? shortUrl.longUrl} + {shortUrl.title && ( + + + + )} {renderTags(shortUrl.tags)} (selectedServer: SelectedServer): boolean => + isReachableServer(selectedServer) && versionMatch(selectedServer.version, versions); + +export const titleIsSupported = serverMatchesVersions({ minVersion: '2.6.0' }); diff --git a/test/short-urls/ShortUrlsTable.test.tsx b/test/short-urls/ShortUrlsTable.test.tsx index d486e4f0..74a736b3 100644 --- a/test/short-urls/ShortUrlsTable.test.tsx +++ b/test/short-urls/ShortUrlsTable.test.tsx @@ -2,23 +2,26 @@ 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 { OrderableFields, SORTABLE_FIELDS } from '../../src/short-urls/reducers/shortUrlsListParams'; import { ShortUrlsList } from '../../src/short-urls/reducers/shortUrlsList'; +import { ReachableServer, SelectedServer } from '../../src/servers/data'; describe('', () => { let wrapper: ShallowWrapper; const shortUrlsList = Mock.all(); const orderByColumn = jest.fn(); const ShortUrlsRow = () => null; - const ShortUrlsTable = shortUrlsTableCreator(ShortUrlsRow); - beforeEach(() => { + const createWrapper = (server: SelectedServer = null) => { wrapper = shallow( - orderByColumn} />, + orderByColumn} />, ); - }); + return wrapper; + }; + + beforeEach(() => createWrapper()); afterEach(jest.resetAllMocks); afterEach(() => wrapper?.unmount()); @@ -42,13 +45,13 @@ describe('', () => { }); }); - it('should render 6 table header cells with conditional order by icon', () => { - const getThElementForSortableField = (sortableField: string) => wrapper.find('table') + it('should render table header cells with conditional order by icon', () => { + const getThElementForSortableField = (orderableField: 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); + .filterWhere((e) => e.text().includes(SORTABLE_FIELDS[orderableField as OrderableFields])); + const sortableFields = Object.keys(SORTABLE_FIELDS).filter((sortableField) => sortableField !== 'title'); expect.assertions(sortableFields.length); sortableFields.forEach((sortableField) => { @@ -56,4 +59,18 @@ describe('', () => { expect(orderByColumn).toHaveBeenCalled(); }); }); + + it.each([ + [ '2.6.0' ], + [ '2.6.1' ], + [ '2.7.0' ], + [ '3.0.0' ], + ])('should render composed column when server supports title', (version) => { + const wrapper = createWrapper(Mock.of({ version })); + const composedColumn = wrapper.find('table').find('th').at(2); + const text = composedColumn.text(); + + expect(text).toContain('Title'); + expect(text).toContain('Long URL'); + }); }); From f15bbcd0277427b1ec3cdf384c12af637fffc1d2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 5 Mar 2021 15:23:38 +0100 Subject: [PATCH 2/4] Improved ShortUrlsRow test --- test/short-urls/helpers/ShortUrlsRow.test.tsx | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/test/short-urls/helpers/ShortUrlsRow.test.tsx b/test/short-urls/helpers/ShortUrlsRow.test.tsx index e5429a7c..a929467c 100644 --- a/test/short-urls/helpers/ShortUrlsRow.test.tsx +++ b/test/short-urls/helpers/ShortUrlsRow.test.tsx @@ -22,9 +22,7 @@ describe('', () => { getColorForKey: jest.fn(), setColorForKey: jest.fn(), }); - const server = Mock.of({ - url: 'https://doma.in', - }); + const server = Mock.of({ url: 'https://doma.in' }); const shortUrl: ShortUrl = { shortCode: 'abc123', shortUrl: 'http://doma.in/abc123', @@ -39,14 +37,29 @@ describe('', () => { maxVisits: null, }, }; + const ShortUrlsRow = createShortUrlsRow(ShortUrlsRowMenu, colorGenerator, useStateFlagTimeout); + const createWrapper = (title?: string | null) => { + wrapper = shallow( + , + ); - beforeEach(() => { - const ShortUrlsRow = createShortUrlsRow(ShortUrlsRowMenu, colorGenerator, useStateFlagTimeout); + return wrapper; + }; - wrapper = shallow(); - }); + beforeEach(() => createWrapper()); afterEach(() => wrapper.unmount()); + it.each([ + [ null, 6 ], + [ undefined, 6 ], + [ 'The title', 7 ], + ])('renders expected amount of columns', (title, expectedAmount) => { + const wrapper = createWrapper(title); + const cols = wrapper.find('td'); + + expect(cols).toHaveLength(expectedAmount); + }); + it('renders date in first column', () => { const col = wrapper.find('td').first(); const moment = col.find(Moment); @@ -68,6 +81,20 @@ describe('', () => { expect(link.prop('href')).toEqual(shortUrl.longUrl); }); + it('renders title when short URL has it', () => { + const wrapper = createWrapper('My super cool title'); + const cols = wrapper.find('td'); + const titleSharedCol = cols.at(2).find(ExternalLink); + const dedicatedShortUrlCol = cols.at(3).find(ExternalLink); + + expect(titleSharedCol).toHaveLength(1); + expect(dedicatedShortUrlCol).toHaveLength(1); + expect(titleSharedCol.prop('href')).toEqual(shortUrl.longUrl); + expect(dedicatedShortUrlCol.prop('href')).toEqual(shortUrl.longUrl); + expect(titleSharedCol.html()).toContain('My super cool title'); + expect(dedicatedShortUrlCol.prop('children')).not.toBeDefined(); + }); + describe('renders list of tags in fourth row', () => { it('with tags', () => { const col = wrapper.find('td').at(3); From df87ad58670e91cc0198eeace89ac9d4f526ef4f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 5 Mar 2021 15:24:38 +0100 Subject: [PATCH 3/4] Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 583a6b02..66806cfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * [#385](https://github.com/shlinkio/shlink-web-client/issues/385) Added setting to determine if "validate URL" should be enabled or disabled by default. * [#386](https://github.com/shlinkio/shlink-web-client/issues/386) Added new card in overview section to display amount of orphan visits when using Shlink 2.6.0 or higher. * [#177](https://github.com/shlinkio/shlink-web-client/issues/177) Added dark theme. -* [#387](https://github.com/shlinkio/shlink-web-client/issues/387) Added section to see orphan visits stats, when consuming Shlink >=2.6.0. +* [#387](https://github.com/shlinkio/shlink-web-client/issues/387) Added a section to see orphan visits stats, when consuming Shlink >=2.6.0. +* [#383](https://github.com/shlinkio/shlink-web-client/issues/383) Added title to short URLs list, displayed when consuming Shlink >=2.6.0. ### Changed * [#382](https://github.com/shlinkio/shlink-web-client/issues/382) Ensured short URL tags are edited through the `PATCH /short-urls/{shortCode}` endpoint when using Shlink 2.6.0 or higher. From 87a32b412f0e788e17375d10c8d522138fdb0a16 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 5 Mar 2021 15:44:15 +0100 Subject: [PATCH 4/4] Added short URL title to visits header --- src/index.scss | 3 +- src/utils/base.scss | 2 +- src/visits/ShortUrlVisitsHeader.tsx | 5 +-- test/visits/ShortUrlVisitsHeader.test.tsx | 39 +++++++++++++++-------- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/index.scss b/src/index.scss index 65ebc4f1..5bcc10ae 100644 --- a/src/index.scss +++ b/src/index.scss @@ -61,7 +61,8 @@ body, .dropdown-divider, .dropdown-menu, .list-group-item, -.modal-content { +.modal-content, +hr { border-color: var(--border-color); } diff --git a/src/utils/base.scss b/src/utils/base.scss index d5c52b2c..ba6ff54c 100644 --- a/src/utils/base.scss +++ b/src/utils/base.scss @@ -18,7 +18,7 @@ $mediumGrey: #dee2e6; $textPlaceholder: #6c757d; // Misc -$headerHeight: 57px; +$headerHeight: 56px; $asideMenuWidth: 260px; $footer-height: 2.3rem; $footer-margin: .8rem; diff --git a/src/visits/ShortUrlVisitsHeader.tsx b/src/visits/ShortUrlVisitsHeader.tsx index d5e8973c..38826b8e 100644 --- a/src/visits/ShortUrlVisitsHeader.tsx +++ b/src/visits/ShortUrlVisitsHeader.tsx @@ -17,6 +17,7 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortU const { visits } = shortUrlVisits; const shortLink = shortUrl?.shortUrl ?? ''; const longLink = shortUrl?.longUrl ?? ''; + const title = shortUrl?.title; const renderDate = () => !shortUrl ? Loading... : ( @@ -39,9 +40,9 @@ const ShortUrlVisitsHeader = ({ shortUrlDetail, shortUrlVisits, goBack }: ShortU
Created: {renderDate()}
- Long URL:{' '} + {`${title ? 'Title' : 'Long URL'}: `} {loading && Loading...} - {!loading && } + {!loading && {title ?? longLink}}
); diff --git a/test/visits/ShortUrlVisitsHeader.test.tsx b/test/visits/ShortUrlVisitsHeader.test.tsx index cd68cfd2..c2701eba 100644 --- a/test/visits/ShortUrlVisitsHeader.test.tsx +++ b/test/visits/ShortUrlVisitsHeader.test.tsx @@ -8,35 +8,48 @@ import { ShortUrlVisits } from '../../src/visits/reducers/shortUrlVisits'; describe('', () => { let wrapper: ShallowWrapper; - const shortUrlDetail = Mock.of({ - shortUrl: { - shortUrl: 'https://doma.in/abc123', - longUrl: 'https://foo.bar/bar/foo', - dateCreated: '2018-01-01T10:00:00+01:00', - }, - loading: false, - }); + const dateCreated = '2018-01-01T10:00:00+01:00'; + const longUrl = 'https://foo.bar/bar/foo'; const shortUrlVisits = Mock.of({ visits: [{}, {}, {}], }); const goBack = jest.fn(); + const createWrapper = (title?: string | null) => { + const shortUrlDetail = Mock.of({ + shortUrl: { + shortUrl: 'https://doma.in/abc123', + longUrl, + dateCreated, + title, + }, + loading: false, + }); - beforeEach(() => { wrapper = shallow( , ); - }); + + return wrapper; + }; + + beforeEach(() => createWrapper()); afterEach(() => wrapper.unmount()); it('shows when the URL was created', () => { const moment = wrapper.find(Moment).first(); - expect(moment.prop('children')).toEqual(shortUrlDetail.shortUrl?.dateCreated); + expect(moment.prop('children')).toEqual(dateCreated); }); - it('shows the long URL', () => { + it.each([ + [ null, longUrl ], + [ undefined, longUrl ], + [ 'My cool title', 'My cool title' ], + ])('shows the long URL and title', (title, expectedContent) => { + const wrapper = createWrapper(title); const longUrlLink = wrapper.find(ExternalLink).last(); - expect(longUrlLink.prop('href')).toEqual(shortUrlDetail.shortUrl?.longUrl); + expect(longUrlLink.prop('href')).toEqual(longUrl); + expect(longUrlLink.prop('children')).toEqual(expectedContent); }); });