From b61d86335672962becbd97c03984f14b1ae5644f Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Wed, 1 Sep 2021 10:53:45 +0200 Subject: [PATCH 1/7] Fixed merge conflicts --- CHANGELOG.md | 23 ++++++++++++++++++++--- src/short-urls/ShortUrlsList.tsx | 2 +- src/tags/TagCard.tsx | 2 +- test/tags/TagCard.test.tsx | 24 +++++++++++++++--------- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 363b2b14..484e39d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org). +## [Unreleased] +### Added +* *Nothing* + +### Changed +* *Nothing* + +### Deprecated +* *Nothing* + +### Removed +* *Nothing* + +### Fixed +* [#478](https://github.com/shlinkio/shlink-web-client/pull/478) Fixed tags including special chars not being properly URL encoded before using them as query params. + + ## [3.2.0] - 2021-07-12 ### Added * [#433](https://github.com/shlinkio/shlink-web-client/pull/433) Added support to provide a default server to connect to via env vars: - * `SHLINK_SERVER_URL`: The URL of the Shlink server to configure by default. - * `SHLINK_SERVER_API_KEY`: The API key of the Shlink server. - * `SHLINK_SERVER_NAME`: A name you want to give to this server. Defaults to *Shlink* if not provided. + * `SHLINK_SERVER_URL`: The URL of the Shlink server to configure by default. + * `SHLINK_SERVER_API_KEY`: The API key of the Shlink server. + * `SHLINK_SERVER_NAME`: A name you want to give to this server. Defaults to *Shlink* if not provided. * [#432](https://github.com/shlinkio/shlink-web-client/pull/432) Added support to provide the `servers.json` file inside a `conf.d` folder. * [#440](https://github.com/shlinkio/shlink-web-client/pull/440) Added hint of what visits come potentially from a bot, in the visits table, when consuming Shlink >=2.7. diff --git a/src/short-urls/ShortUrlsList.tsx b/src/short-urls/ShortUrlsList.tsx index 4736bc8c..ef24eb5b 100644 --- a/src/short-urls/ShortUrlsList.tsx +++ b/src/short-urls/ShortUrlsList.tsx @@ -70,7 +70,7 @@ const ShortUrlsList = (ShortUrlsTable: FC) => boundToMercur useEffect(() => { const { tag } = parseQuery<{ tag?: string }>(location.search); - const tags = tag ? [ tag ] : shortUrlsListParams.tags; + const tags = tag ? [ decodeURIComponent(tag) ] : shortUrlsListParams.tags; refreshList({ page: match.params.page, tags, itemsPerPage: undefined }); diff --git a/src/tags/TagCard.tsx b/src/tags/TagCard.tsx index b72bca0e..05b4f13e 100644 --- a/src/tags/TagCard.tsx +++ b/src/tags/TagCard.tsx @@ -30,7 +30,7 @@ const TagCard = ( const [ isEditModalOpen, toggleEdit ] = useToggle(); const serverId = isServerWithId(selectedServer) ? selectedServer.id : ''; - const shortUrlsLink = `/server/${serverId}/list-short-urls/1?tag=${tag}`; + const shortUrlsLink = `/server/${serverId}/list-short-urls/1?tag=${encodeURIComponent(tag)}`; return ( diff --git a/test/tags/TagCard.test.tsx b/test/tags/TagCard.test.tsx index 38d1abbe..db1dce18 100644 --- a/test/tags/TagCard.test.tsx +++ b/test/tags/TagCard.test.tsx @@ -14,30 +14,36 @@ describe('', () => { }; const DeleteTagConfirmModal = jest.fn(); const EditTagModal = jest.fn(); - - beforeEach(() => { - const TagCard = createTagCard(DeleteTagConfirmModal, EditTagModal, () => null, Mock.all()); - + const TagCard = createTagCard(DeleteTagConfirmModal, EditTagModal, () => null, Mock.all()); + const createWrapper = (tag = 'ssr') => { wrapper = shallow( ({ id: '1' })} tagStats={tagStats} displayed={true} toggle={() => {}} />, ); - }); + + return wrapper; + }; + + beforeEach(() => createWrapper()); afterEach(() => wrapper.unmount()); afterEach(jest.resetAllMocks); - it('shows a TagBullet and a link to the list filtering by the tag', () => { + it.each([ + [ 'ssr', '/server/1/list-short-urls/1?tag=ssr' ], + [ 'ssr-&-foo', '/server/1/list-short-urls/1?tag=ssr-%26-foo' ], + ])('shows a TagBullet and a link to the list filtering by the tag', (tag, expectedLink) => { + const wrapper = createWrapper(tag); const links = wrapper.find(Link); const bullet = wrapper.find(TagBullet); - expect(links.at(0).prop('to')).toEqual('/server/1/list-short-urls/1?tag=ssr'); - expect(bullet.prop('tag')).toEqual('ssr'); + expect(links.at(0).prop('to')).toEqual(expectedLink); + expect(bullet.prop('tag')).toEqual(tag); }); it('displays delete modal when delete btn is clicked', () => { From 7330fd85ff91ccf81c985a8f88ae4bd5590245bd Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 12 Sep 2021 09:34:51 +0200 Subject: [PATCH 2/7] Updated function signaure --- src/servers/helpers/ImportServersBtn.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/servers/helpers/ImportServersBtn.tsx b/src/servers/helpers/ImportServersBtn.tsx index 5f46641e..490d2d7e 100644 --- a/src/servers/helpers/ImportServersBtn.tsx +++ b/src/servers/helpers/ImportServersBtn.tsx @@ -7,7 +7,7 @@ type Ref = RefObject | MutableRefObject; export interface ImportServersBtnProps { onImport?: () => void; - onImportError?: () => void; + onImportError?: (error: Error) => void; } interface ImportServersBtnConnectProps extends ImportServersBtnProps { From c6cca9c91f85b7934d133feac8b836baa61fbb89 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 12 Sep 2021 09:56:29 +0200 Subject: [PATCH 3/7] Fixed indentation --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 484e39d6..2bd4a0e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,9 +25,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added * [#433](https://github.com/shlinkio/shlink-web-client/pull/433) Added support to provide a default server to connect to via env vars: - * `SHLINK_SERVER_URL`: The URL of the Shlink server to configure by default. - * `SHLINK_SERVER_API_KEY`: The API key of the Shlink server. - * `SHLINK_SERVER_NAME`: A name you want to give to this server. Defaults to *Shlink* if not provided. + * `SHLINK_SERVER_URL`: The URL of the Shlink server to configure by default. + * `SHLINK_SERVER_API_KEY`: The API key of the Shlink server. + * `SHLINK_SERVER_NAME`: A name you want to give to this server. Defaults to *Shlink* if not provided. * [#432](https://github.com/shlinkio/shlink-web-client/pull/432) Added support to provide the `servers.json` file inside a `conf.d` folder. * [#440](https://github.com/shlinkio/shlink-web-client/pull/440) Added hint of what visits come potentially from a bot, in the visits table, when consuming Shlink >=2.7. From 91e003153b32a3a0604d169064c72abb20f2f573 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 12 Sep 2021 09:54:17 +0200 Subject: [PATCH 4/7] Updated logic to import servers, to not check the file type --- src/servers/services/ServersImporter.ts | 32 +++++---- test/servers/services/ServersImporter.test.ts | 67 ++++++++++++++++--- 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/src/servers/services/ServersImporter.ts b/src/servers/services/ServersImporter.ts index 7dcf8175..28f43b65 100644 --- a/src/servers/services/ServersImporter.ts +++ b/src/servers/services/ServersImporter.ts @@ -1,29 +1,37 @@ import { CsvJson } from 'csvjson'; import { ServerData } from '../data'; -interface CsvFile extends File { - type: 'text/csv' | 'text/comma-separated-values' | 'application/csv'; -} +const validateServer = (server: any): server is ServerData => + typeof server.url === 'string' && typeof server.apiKey === 'string' && typeof server.name === 'string'; -const CSV_MIME_TYPES = [ 'text/csv', 'text/comma-separated-values', 'application/csv' ]; -const isCsv = (file?: File | null): file is CsvFile => !!file && CSV_MIME_TYPES.includes(file.type); +const validateServers = (servers: any): servers is ServerData[] => + Array.isArray(servers) && servers.every(validateServer); export default class ServersImporter { - public constructor(private readonly csvjson: CsvJson, private readonly fileReaderFactory: () => FileReader) {} + public constructor(private readonly csvJson: CsvJson, private readonly fileReaderFactory: () => FileReader) {} public readonly importServersFromFile = async (file?: File | null): Promise => { - if (!isCsv(file)) { - throw new Error('No file provided or file is not a CSV'); + if (!file) { + throw new Error('No file provided'); } const reader = this.fileReaderFactory(); - return new Promise((resolve) => { + return new Promise((resolve, reject) => { reader.addEventListener('loadend', (e: ProgressEvent) => { - const content = e.target?.result?.toString() ?? ''; - const servers = this.csvjson.toObject(content); + try { + // TODO Read as stream, otherwise, if the file is too big, this will block the browser tab + const content = e.target?.result?.toString() ?? ''; + const servers = this.csvJson.toObject(content); - resolve(servers); + if (!validateServers(servers)) { + throw new Error('Provided file does not have the right format.'); + } + + resolve(servers); + } catch (e) { + reject(e); + } }); reader.readAsText(file); }); diff --git a/test/servers/services/ServersImporter.test.ts b/test/servers/services/ServersImporter.test.ts index 60e5dfc4..3f26ca96 100644 --- a/test/servers/services/ServersImporter.test.ts +++ b/test/servers/services/ServersImporter.test.ts @@ -21,23 +21,70 @@ describe('ServersImporter', () => { describe('importServersFromFile', () => { it('rejects with error if no file was provided', async () => { await expect(importer.importServersFromFile()).rejects.toEqual( - new Error('No file provided or file is not a CSV'), + new Error('No file provided'), ); }); - it('rejects with error if provided file is not a CSV', async () => { - await expect(importer.importServersFromFile(Mock.of({ type: 'text/html' }))).rejects.toEqual( - new Error('No file provided or file is not a CSV'), - ); + it('rejects with error if parsing the file fails', async () => { + const expectedError = new Error('Error parsing file'); + + toObject.mockImplementation(() => { + throw expectedError; + }); + + await expect(importer.importServersFromFile(Mock.of({ type: 'text/html' }))).rejects.toEqual(expectedError); }); it.each([ - [ 'text/csv' ], - [ 'text/comma-separated-values' ], - [ 'application/csv' ], - ])('reads file when a CSV is provided', async (type) => { - await importer.importServersFromFile(Mock.of({ type })); + [{}], + [ undefined ], + [[{ foo: 'bar' }]], + [ + [ + { + url: 1, + apiKey: 1, + name: 1, + }, + ], + ], + [ + [ + { + url: 'foo', + apiKey: 'foo', + name: 'foo', + }, + { bar: 'foo' }, + ], + ], + ])('rejects with error if provided file does not parse to valid list of servers', async (parsedObject) => { + toObject.mockReturnValue(parsedObject); + await expect(importer.importServersFromFile(Mock.of({ type: 'text/html' }))).rejects.toEqual( + new Error('Provided file does not have the right format.'), + ); + }); + + it('reads file when a CSV containing valid servers is provided', async () => { + const expectedServers = [ + { + url: 'foo', + apiKey: 'foo', + name: 'foo', + }, + { + url: 'bar', + apiKey: 'bar', + name: 'bar', + }, + ]; + + toObject.mockReturnValue(expectedServers); + + const result = await importer.importServersFromFile(Mock.all()); + + expect(result).toEqual(expectedServers); expect(readAsText).toHaveBeenCalledTimes(1); expect(toObject).toHaveBeenCalledTimes(1); }); From d2ad1cd54bfa88b46c9f4f65cf40dd7b40b7a1a8 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 12 Sep 2021 09:58:24 +0200 Subject: [PATCH 5/7] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bd4a0e3..ee249394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed * [#478](https://github.com/shlinkio/shlink-web-client/pull/478) Fixed tags including special chars not being properly URL encoded before using them as query params. +* [#480](https://github.com/shlinkio/shlink-web-client/pull/480) Fixed servers import on Chromium-based browsers when using windows. ## [3.2.0] - 2021-07-12 From 178f15b7d3725829c1df7e9949d0c186fc04bab1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 12 Sep 2021 10:16:05 +0200 Subject: [PATCH 6/7] Ensured end dates are set at the end of the date when filtering visits --- src/utils/dates/types/index.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/utils/dates/types/index.ts b/src/utils/dates/types/index.ts index 86767dc4..88672d36 100644 --- a/src/utils/dates/types/index.ts +++ b/src/utils/dates/types/index.ts @@ -55,6 +55,7 @@ export const rangeOrIntervalToString = (range?: DateRange | DateInterval): strin }; const startOfDaysAgo = (daysAgo: number) => startOfDay(subDays(new Date(), daysAgo)); +const endingToday = (startDate: Date): DateRange => ({ startDate, endDate: endOfDay(new Date()) }); export const intervalToDateRange = (dateInterval?: DateInterval): DateRange => { if (!dateInterval) { @@ -63,19 +64,19 @@ export const intervalToDateRange = (dateInterval?: DateInterval): DateRange => { switch (dateInterval) { case 'today': - return { startDate: startOfDay(new Date()), endDate: new Date() }; + return endingToday(startOfDay(new Date())); case 'yesterday': return { startDate: startOfDaysAgo(1), endDate: endOfDay(subDays(new Date(), 1)) }; case 'last7Days': - return { startDate: startOfDaysAgo(7), endDate: new Date() }; + return endingToday(startOfDaysAgo(7)); case 'last30Days': - return { startDate: startOfDaysAgo(30), endDate: new Date() }; + return endingToday(startOfDaysAgo(30)); case 'last90Days': - return { startDate: startOfDaysAgo(90), endDate: new Date() }; + return endingToday(startOfDaysAgo(90)); case 'last180days': - return { startDate: startOfDaysAgo(180), endDate: new Date() }; + return endingToday(startOfDaysAgo(180)); case 'last365Days': - return { startDate: startOfDaysAgo(365), endDate: new Date() }; + return endingToday(startOfDaysAgo(365)); } return {}; From 66c91722fccf0e6acbf1cba06c028f993174148e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 12 Sep 2021 10:17:24 +0200 Subject: [PATCH 7/7] Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee249394..c13911ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org). -## [Unreleased] +## [3.2.1] - 2021-09-12 ### Added * *Nothing* @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed * [#478](https://github.com/shlinkio/shlink-web-client/pull/478) Fixed tags including special chars not being properly URL encoded before using them as query params. * [#480](https://github.com/shlinkio/shlink-web-client/pull/480) Fixed servers import on Chromium-based browsers when using windows. +* [#482](https://github.com/shlinkio/shlink-web-client/pull/480) Fixed end date not being set to the end of the day when filtering visits using a "smart filter" (last 7 days, last 30 days, etc). ## [3.2.0] - 2021-07-12