diff --git a/CHANGELOG.md b/CHANGELOG.md index bfd3df0b..32c22647 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,24 +4,34 @@ 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] +## [4.0.0] - 2024-01-29 ### Added -* *Nothing* +* [shlink-web-component #7](https://github.com/shlinkio/shlink-web-component/issues/7) Allow comparing visits for multiple short URLs, tags or domains. + + When in the tags, domains or short URLs tables, you can now pick up to 5 items to compare their visits. Once selected, you are taken to a section displaying a comparative line chart, which supports all regular visits filtering capabilities. + +* [shlink-web-component #9](https://github.com/shlinkio/shlink-web-component/issues/9) Allow comparing visits with the previous period. +* [shlink-web-component #12](https://github.com/shlinkio/shlink-web-component/issues/12) and [#13](https://github.com/shlinkio/shlink-web-component/issues/13) Add new "Visits options" section for arbitrary visit stats options. Add section to delete short URL and orphan visits there. + + This section is only visible if short URL visits deletion or orphan visits deletion are supported by connected Shlink server. + +* [shlink-web-component #10](https://github.com/shlinkio/shlink-web-component/issues/10) Improve general accessibility: Add accessibility tests, fix accessibility issues and enable accessibility linting rules. ### Changed * [#338](https://github.com/shlinkio/shlink-web-client/issues/338) Extract `@shlinkio/shlink-web-component` and `@shlinkio/shlink-frontend-kit` as external libs. * [#978](https://github.com/shlinkio/shlink-web-client/issues/978) Use system preferred theme as default theme. * Use API client from `@shlinkio/shlink-js-sdk` to consume Shlink servers. * [#902](https://github.com/shlinkio/shlink-web-client/pull/902) Docker image is no longer running as root. As a side effect, exposed port is `8080`, not `80` anymore. +* [shlink-web-component #117](https://github.com/shlinkio/shlink-web-component/issues/117) Migrate charts from Chart.JS to Recharts. ### Deprecated * *Nothing* ### Removed -* *Nothing* +* Drop support for Shlink older than v3.0.0 ### Fixed -* [#910](https://github.com/shlinkio/shlink-web-client/issues/910) Fix warnings related with missing `act`in tests and refs in `AppUpdateBanner`. +* [#910](https://github.com/shlinkio/shlink-web-client/issues/910) Fix warnings related with missing `act` in tests and refs in `AppUpdateBanner`. ## [3.10.2] - 2023-07-09 diff --git a/package-lock.json b/package-lock.json index 6e1309fb..44950641 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@shlinkio/data-manipulation": "^1.0.3", "@shlinkio/shlink-frontend-kit": "^0.4.2", "@shlinkio/shlink-js-sdk": "^0.2.2", - "@shlinkio/shlink-web-component": "^0.4.1", + "@shlinkio/shlink-web-component": "^0.5.0", "bootstrap": "5.2.3", "bottlejs": "^2.0.1", "clsx": "^2.1.0", @@ -2980,26 +2980,26 @@ "integrity": "sha512-gY9EiaULbEwmrTsnXk0MQUG/3bOvhxHQNOU35psFX2NbB8OzdfoE1iiT/Sez3awmmxz+/p8d6aULBt/ywxukIQ==" }, "node_modules/@shlinkio/shlink-web-component": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@shlinkio/shlink-web-component/-/shlink-web-component-0.4.1.tgz", - "integrity": "sha512-jzzrbe6ufzF6X1JTqZAjLr5eAtfhJDseglqXVdQ+UXLNYvvGcxrp5LlTxsnf9LQUgLkvZ1qfz5Knfk143B1PtA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@shlinkio/shlink-web-component/-/shlink-web-component-0.5.0.tgz", + "integrity": "sha512-YuZ7VSJtGpHUVUoO1Zvdmn0P5msbxh8xqiJVyvKx9F/x5uvodRIHuUypR3n/A69DfvYt9SUagGZAz3BtG1UB8g==", "dependencies": { - "@json2csv/plainjs": "^7.0.4", - "@shlinkio/data-manipulation": "^1.0.2", + "@json2csv/plainjs": "^7.0.5", + "@shlinkio/data-manipulation": "^1.0.3", "bottlejs": "^2.0.1", "bowser": "^2.11.0", - "clsx": "^2.0.0", + "clsx": "^2.1.0", "compare-versions": "^6.1.0", - "date-fns": "^2.30.0", + "date-fns": "^3.3.1", "event-source-polyfill": "^1.0.31", "leaflet": "^1.9.4", "react-copy-to-clipboard": "^5.1.0", - "react-datepicker": "^4.24.0", + "react-datepicker": "^4.25.0", "react-external-link": "^2.2.0", "react-leaflet": "^4.2.1", "react-swipeable": "^7.0.1", "react-tag-autocomplete": "^7.1.0", - "recharts": "^2.10.3" + "recharts": "^2.10.4" }, "peerDependencies": { "@fortawesome/fontawesome-svg-core": "^6.4.2", @@ -3008,12 +3008,12 @@ "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "^0.2.0", "@reduxjs/toolkit": "^2.0.1", - "@shlinkio/shlink-frontend-kit": "^0.4.0", - "@shlinkio/shlink-js-sdk": "^0.2.0", + "@shlinkio/shlink-frontend-kit": "^0.4.2", + "@shlinkio/shlink-js-sdk": "^0.2.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^9.0.1", - "react-router-dom": "^6.14.2", + "react-router-dom": "^6.20.1", "reactstrap": "^9.2.0" }, "peerDependenciesMeta": { @@ -3022,21 +3022,6 @@ } } }, - "node_modules/@shlinkio/shlink-web-component/node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, "node_modules/@shlinkio/stylelint-config-css-coding-standard": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@shlinkio/stylelint-config-css-coding-standard/-/stylelint-config-css-coding-standard-1.1.1.tgz", @@ -8713,9 +8698,9 @@ } }, "node_modules/react-datepicker": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.24.0.tgz", - "integrity": "sha512-2QUC2pP+x4v3Jp06gnFllxKsJR0yoT/K6y86ItxEsveTXUpsx+NBkChWXjU0JsGx/PL8EQnsxN0wHl4zdA1m/g==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.25.0.tgz", + "integrity": "sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==", "dependencies": { "@popperjs/core": "^2.11.8", "classnames": "^2.2.6", @@ -9031,9 +9016,9 @@ } }, "node_modules/recharts": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.10.3.tgz", - "integrity": "sha512-G4J96fKTZdfFQd6aQnZjo2nVNdXhp+uuLb00+cBTGLo85pChvm1+E67K3wBOHDE/77spcYb2Cy9gYWVqiZvQCg==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.11.0.tgz", + "integrity": "sha512-5s+u1m5Hwxb2nh0LABkE3TS/lFqFHyWl7FnPbQhHobbQQia4ih1t3o3+ikPYr31Ns+kYe4FASIthKeKi/YYvMg==", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", @@ -10432,9 +10417,9 @@ } }, "node_modules/victory-vendor": { - "version": "36.7.0", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.7.0.tgz", - "integrity": "sha512-nqYuTkLSdTTeACyXcCLbL7rl0y6jpzLPtTNGOtSnajdR+xxMxBdjMxDjfNJNlhR+ZU8vbXz+QejntcbY7h9/ZA==", + "version": "36.8.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.8.2.tgz", + "integrity": "sha512-NfSQi7ISCdBbDpn3b6rg+8RpFZmWIM9mcks48BbogHE2F6h1XKdA34oiCKP5hP1OGvTotDRzsexiJKzrK4Exuw==", "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", @@ -13134,36 +13119,26 @@ "integrity": "sha512-gY9EiaULbEwmrTsnXk0MQUG/3bOvhxHQNOU35psFX2NbB8OzdfoE1iiT/Sez3awmmxz+/p8d6aULBt/ywxukIQ==" }, "@shlinkio/shlink-web-component": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@shlinkio/shlink-web-component/-/shlink-web-component-0.4.1.tgz", - "integrity": "sha512-jzzrbe6ufzF6X1JTqZAjLr5eAtfhJDseglqXVdQ+UXLNYvvGcxrp5LlTxsnf9LQUgLkvZ1qfz5Knfk143B1PtA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@shlinkio/shlink-web-component/-/shlink-web-component-0.5.0.tgz", + "integrity": "sha512-YuZ7VSJtGpHUVUoO1Zvdmn0P5msbxh8xqiJVyvKx9F/x5uvodRIHuUypR3n/A69DfvYt9SUagGZAz3BtG1UB8g==", "requires": { - "@json2csv/plainjs": "^7.0.4", - "@shlinkio/data-manipulation": "^1.0.2", + "@json2csv/plainjs": "^7.0.5", + "@shlinkio/data-manipulation": "^1.0.3", "bottlejs": "^2.0.1", "bowser": "^2.11.0", - "clsx": "^2.0.0", + "clsx": "^2.1.0", "compare-versions": "^6.1.0", - "date-fns": "^2.30.0", + "date-fns": "^3.3.1", "event-source-polyfill": "^1.0.31", "leaflet": "^1.9.4", "react-copy-to-clipboard": "^5.1.0", - "react-datepicker": "^4.24.0", + "react-datepicker": "^4.25.0", "react-external-link": "^2.2.0", "react-leaflet": "^4.2.1", "react-swipeable": "^7.0.1", "react-tag-autocomplete": "^7.1.0", - "recharts": "^2.10.3" - }, - "dependencies": { - "date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "requires": { - "@babel/runtime": "^7.21.0" - } - } + "recharts": "^2.10.4" } }, "@shlinkio/stylelint-config-css-coding-standard": { @@ -17015,9 +16990,9 @@ } }, "react-datepicker": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.24.0.tgz", - "integrity": "sha512-2QUC2pP+x4v3Jp06gnFllxKsJR0yoT/K6y86ItxEsveTXUpsx+NBkChWXjU0JsGx/PL8EQnsxN0wHl4zdA1m/g==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.25.0.tgz", + "integrity": "sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==", "requires": { "@popperjs/core": "^2.11.8", "classnames": "^2.2.6", @@ -17221,9 +17196,9 @@ } }, "recharts": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.10.3.tgz", - "integrity": "sha512-G4J96fKTZdfFQd6aQnZjo2nVNdXhp+uuLb00+cBTGLo85pChvm1+E67K3wBOHDE/77spcYb2Cy9gYWVqiZvQCg==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.11.0.tgz", + "integrity": "sha512-5s+u1m5Hwxb2nh0LABkE3TS/lFqFHyWl7FnPbQhHobbQQia4ih1t3o3+ikPYr31Ns+kYe4FASIthKeKi/YYvMg==", "requires": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", @@ -18209,9 +18184,9 @@ } }, "victory-vendor": { - "version": "36.7.0", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.7.0.tgz", - "integrity": "sha512-nqYuTkLSdTTeACyXcCLbL7rl0y6jpzLPtTNGOtSnajdR+xxMxBdjMxDjfNJNlhR+ZU8vbXz+QejntcbY7h9/ZA==", + "version": "36.8.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.8.2.tgz", + "integrity": "sha512-NfSQi7ISCdBbDpn3b6rg+8RpFZmWIM9mcks48BbogHE2F6h1XKdA34oiCKP5hP1OGvTotDRzsexiJKzrK4Exuw==", "requires": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", diff --git a/package.json b/package.json index e2b6aeb8..70ff0904 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@shlinkio/data-manipulation": "^1.0.3", "@shlinkio/shlink-frontend-kit": "^0.4.2", "@shlinkio/shlink-js-sdk": "^0.2.2", - "@shlinkio/shlink-web-component": "^0.4.1", + "@shlinkio/shlink-web-component": "^0.5.0", "bootstrap": "5.2.3", "bottlejs": "^2.0.1", "clsx": "^2.1.0", diff --git a/src/settings/VisitsSettings.tsx b/src/settings/VisitsSettings.tsx index 115b2e45..483ffe64 100644 --- a/src/settings/VisitsSettings.tsx +++ b/src/settings/VisitsSettings.tsx @@ -1,39 +1,60 @@ import { LabeledFormGroup, SimpleCard, ToggleSwitch } from '@shlinkio/shlink-frontend-kit'; import type { Settings, VisitsSettings as VisitsSettingsConfig } from '@shlinkio/shlink-web-component'; import type { FC } from 'react'; +import { useCallback } from 'react'; import { FormGroup } from 'reactstrap'; import type { DateInterval } from '../utils/dates/DateIntervalSelector'; import { DateIntervalSelector } from '../utils/dates/DateIntervalSelector'; import { FormText } from '../utils/forms/FormText'; -interface VisitsProps { +type VisitsProps = { settings: Settings; setVisitsSettings: (settings: VisitsSettingsConfig) => void; -} +}; const currentDefaultInterval = (settings: Settings): DateInterval => settings.visits?.defaultInterval ?? 'last30Days'; -export const VisitsSettings: FC = ({ settings, setVisitsSettings }) => ( - - - setVisitsSettings( - { defaultInterval: currentDefaultInterval(settings), excludeBots }, - )} - > - Exclude bots wherever possible (this option‘s effect might depend on Shlink server‘s version). - - The visits coming from potential bots will be {settings.visits?.excludeBots ? 'excluded' : 'included'}. - - - - - setVisitsSettings({ defaultInterval })} - /> - - -); +export const VisitsSettings: FC = ({ settings, setVisitsSettings }) => { + const updateSettings = useCallback( + ({ defaultInterval, ...rest }: Partial) => setVisitsSettings( + { defaultInterval: defaultInterval ?? currentDefaultInterval(settings), ...rest }, + ), + [setVisitsSettings, settings], + ); + + return ( + + + updateSettings({ excludeBots })} + > + Exclude bots wherever possible (this option‘s effect might depend on Shlink server‘s version). + + The visits coming from potential bots will + be {settings.visits?.excludeBots ? 'excluded' : 'included'}. + + + + + updateSettings({ loadPrevInterval })} + > + Compare visits with previous period. + + When loading visits, previous period {settings.visits?.loadPrevInterval ? 'will' : 'won\'t'} be + loaded by default. + + + + + updateSettings({ defaultInterval })} + /> + + + ); +}; diff --git a/test/settings/VisitsSettings.test.tsx b/test/settings/VisitsSettings.test.tsx index 175dc5ac..3f8c8a93 100644 --- a/test/settings/VisitsSettings.test.tsx +++ b/test/settings/VisitsSettings.test.tsx @@ -19,6 +19,7 @@ describe('', () => { expect(screen.getByRole('heading')).toHaveTextContent('Visits'); expect(screen.getByText('Default interval to load on visits sections:')).toBeInTheDocument(); expect(screen.getByText(/^Exclude bots wherever possible/)).toBeInTheDocument(); + expect(screen.getByText('Compare visits with previous period.')).toBeInTheDocument(); }); it.each([ @@ -93,4 +94,36 @@ describe('', () => { await user.click(screen.getByText(/^Exclude bots wherever possible/)); expect(setVisitsSettings).toHaveBeenCalledWith(expect.objectContaining({ excludeBots: true })); }); + + it.each([ + [ + fromPartial({}), + /When loading visits, previous period won't be loaded by default.$/, + /When loading visits, previous period will be loaded by default.$/, + ], + [ + fromPartial({ visits: { loadPrevInterval: false } }), + /When loading visits, previous period won't be loaded by default.$/, + /When loading visits, previous period will be loaded by default.$/, + ], + [ + fromPartial({ visits: { loadPrevInterval: true } }), + /When loading visits, previous period will be loaded by default.$/, + /When loading visits, previous period won't be loaded by default.$/, + ], + ])('displays expected helper text for prev interval control', (settings, expectedText, notExpectedText) => { + setUp(settings); + + const visitsComponent = screen.getByText('Compare visits with previous period.'); + + expect(visitsComponent).toHaveTextContent(expectedText); + expect(visitsComponent).not.toHaveTextContent(notExpectedText); + }); + + it('invokes setVisitsSettings when loading prev visits is toggled', async () => { + const { user } = setUp(); + + await user.click(screen.getByText('Compare visits with previous period.')); + expect(setVisitsSettings).toHaveBeenCalledWith(expect.objectContaining({ loadPrevInterval: true })); + }); });