From d10d7fd96d7b8eef51c19bc9d9e7058adf785f92 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 16 Jun 2025 11:18:03 +0200 Subject: [PATCH 1/2] Replace reactstrap nav bar with tailwind-based one --- CHANGELOG.md | 1 + package-lock.json | 50 ++++++++----------------- package.json | 4 +- src/app/App.tsx | 2 +- src/common/MainHeader.tsx | 54 ++++++++++----------------- src/servers/ServersDropdown.tsx | 50 ++++++++++++------------- src/tailwind.css | 2 + test/common/MainHeader.test.tsx | 39 +++---------------- test/servers/ServersDropdown.test.tsx | 18 +++++---- 9 files changed, 81 insertions(+), 139 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec0b7f9..29841fd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Changed * Update to `@shlinkio/shlink-frontend-kit` 0.9 and `@shlinkio/shlink-web-component` 0.14 to add initial support to the new light theme brand color. +* Replace reactstrap nav bar with `NavBar` component from `@shlinkio/shlink-frontend-kit` ### Deprecated * *Nothing* diff --git a/package-lock.json b/package-lock.json index a75c4bea..76384967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,9 @@ "@json2csv/plainjs": "^7.0.6", "@reduxjs/toolkit": "^2.8.2", "@shlinkio/data-manipulation": "^1.0.3", - "@shlinkio/shlink-frontend-kit": "^0.9.10", + "@shlinkio/shlink-frontend-kit": "^0.9.13", "@shlinkio/shlink-js-sdk": "^2.1.0", - "@shlinkio/shlink-web-component": "^0.14.2", + "@shlinkio/shlink-web-component": "^0.14.3", "bootstrap": "5.2.3", "bottlejs": "^2.0.1", "clsx": "^2.1.1", @@ -3516,9 +3516,9 @@ } }, "node_modules/@shlinkio/shlink-frontend-kit": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.9.10.tgz", - "integrity": "sha512-L1+z3imvoSXHYWaO+H39JXGg40eQW1ytY3hMIE8JUuqJYNmWWLrafmfj1MHenCWGZEhymbQnpGD1yyziy6a9Lw==", + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.9.13.tgz", + "integrity": "sha512-qiEDmrzYA/rmHPdLI9znaYqMKD16ITWon/vG66LlUeDL3zR0Psppy79FjM5k3CmIGsCCZdekXQle6U19YSOQLA==", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.27.12", @@ -3550,9 +3550,9 @@ "license": "MIT" }, "node_modules/@shlinkio/shlink-web-component": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/@shlinkio/shlink-web-component/-/shlink-web-component-0.14.2.tgz", - "integrity": "sha512-4GRT1nLuhVCGuKP8fwRv1EBtgQ2wCvpJqJ6ipYM/QKwA2uJIXChom4TDia+s4X8mESIOjV0++aoPOEr6y6H2iA==", + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@shlinkio/shlink-web-component/-/shlink-web-component-0.14.3.tgz", + "integrity": "sha512-IgTHJYkxp6Pqo4E8waouBXbpytiRasqPyMAMQ5vYfGXU5Y53D4QNkRwPsILkEVTb7B+qwQegyNax4YKjRi/hgA==", "license": "MIT", "dependencies": { "@formkit/drag-and-drop": "^0.5.3", @@ -3569,7 +3569,6 @@ "react-external-link": "^2.5.0", "react-leaflet": "^4.2.1 || ^5.0", "react-swipeable": "^7.0.2", - "react-tag-autocomplete": "^7.5.0", "recharts": "^2.15.3" }, "peerDependencies": { @@ -3579,7 +3578,7 @@ "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", "@reduxjs/toolkit": "^2.5.0", - "@shlinkio/shlink-frontend-kit": "^0.9.10", + "@shlinkio/shlink-frontend-kit": "^0.9.11", "@shlinkio/shlink-js-sdk": "^2.0.0", "react": "^18.3 || ^19.0", "react-dom": "^18.3 || ^19.0", @@ -9400,18 +9399,6 @@ "react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc" } }, - "node_modules/react-tag-autocomplete": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/react-tag-autocomplete/-/react-tag-autocomplete-7.5.0.tgz", - "integrity": "sha512-uy6ncusMKr6p3ip7xb4DTYtF22g7cSRyZq0IeFpgmrQipTbKz4RVFDB5QnnqstN6HTs9cdTtIb3vVuOuOdzH3w==", - "license": "ISC", - "engines": { - "node": ">= 16.12.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" - } - }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -14073,9 +14060,9 @@ "requires": {} }, "@shlinkio/shlink-frontend-kit": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.9.10.tgz", - "integrity": "sha512-L1+z3imvoSXHYWaO+H39JXGg40eQW1ytY3hMIE8JUuqJYNmWWLrafmfj1MHenCWGZEhymbQnpGD1yyziy6a9Lw==", + "version": "0.9.13", + "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.9.13.tgz", + "integrity": "sha512-qiEDmrzYA/rmHPdLI9znaYqMKD16ITWon/vG66LlUeDL3zR0Psppy79FjM5k3CmIGsCCZdekXQle6U19YSOQLA==", "requires": { "@floating-ui/react": "^0.27.12", "clsx": "^2.1.1" @@ -14087,9 +14074,9 @@ "integrity": "sha512-K6zmA/A7Ux9hTn+ZjAm85YmMl7/v5XgZBM62syCxCsK7Tdw7Gg4+C06cZ2gUv+HWrHtv5IXsi4ax00++8Kg5vw==" }, "@shlinkio/shlink-web-component": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/@shlinkio/shlink-web-component/-/shlink-web-component-0.14.2.tgz", - "integrity": "sha512-4GRT1nLuhVCGuKP8fwRv1EBtgQ2wCvpJqJ6ipYM/QKwA2uJIXChom4TDia+s4X8mESIOjV0++aoPOEr6y6H2iA==", + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@shlinkio/shlink-web-component/-/shlink-web-component-0.14.3.tgz", + "integrity": "sha512-IgTHJYkxp6Pqo4E8waouBXbpytiRasqPyMAMQ5vYfGXU5Y53D4QNkRwPsILkEVTb7B+qwQegyNax4YKjRi/hgA==", "requires": { "@formkit/drag-and-drop": "^0.5.3", "@json2csv/plainjs": "^7.0.6", @@ -14105,7 +14092,6 @@ "react-external-link": "^2.5.0", "react-leaflet": "^4.2.1 || ^5.0", "react-swipeable": "^7.0.2", - "react-tag-autocomplete": "^7.5.0", "recharts": "^2.15.3" }, "dependencies": { @@ -18014,12 +18000,6 @@ "integrity": "sha512-v1Qx1l+aC2fdxKa9aKJiaU/ZxmJ5o98RMoFwUqAAzVWUcxgfHFXDDruCKXhw6zIYXm6V64JiHgP9f6mlME5l8w==", "requires": {} }, - "react-tag-autocomplete": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/react-tag-autocomplete/-/react-tag-autocomplete-7.5.0.tgz", - "integrity": "sha512-uy6ncusMKr6p3ip7xb4DTYtF22g7cSRyZq0IeFpgmrQipTbKz4RVFDB5QnnqstN6HTs9cdTtIb3vVuOuOdzH3w==", - "requires": {} - }, "react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index 2da788c0..21d6e481 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,9 @@ "@json2csv/plainjs": "^7.0.6", "@reduxjs/toolkit": "^2.8.2", "@shlinkio/data-manipulation": "^1.0.3", - "@shlinkio/shlink-frontend-kit": "^0.9.10", + "@shlinkio/shlink-frontend-kit": "^0.9.13", "@shlinkio/shlink-js-sdk": "^2.1.0", - "@shlinkio/shlink-web-component": "^0.14.2", + "@shlinkio/shlink-web-component": "^0.14.3", "bootstrap": "5.2.3", "bottlejs": "^2.0.1", "clsx": "^2.1.1", diff --git a/src/app/App.tsx b/src/app/App.tsx index e2d15327..89761e84 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -64,7 +64,7 @@ const App: FCWithDeps = (
-
+
= () => { const { ServersDropdown } = useDependencies(MainHeader); - const { flag: isNotCollapsed, toggle: toggleCollapse, setToFalse: collapse } = useToggle(false, true); - const location = useLocation(); - const { pathname } = location; - - // In mobile devices, collapse the navbar when location changes - useEffect(collapse, [location, collapse]); + const { pathname } = useLocation(); const settingsPath = '/settings'; return ( - - - Shlink - - - - - - - - - - + + Shlink + + )} + > + + Settings + + + ); }; diff --git a/src/servers/ServersDropdown.tsx b/src/servers/ServersDropdown.tsx index 099a2c2e..4db7fb99 100644 --- a/src/servers/ServersDropdown.tsx +++ b/src/servers/ServersDropdown.tsx @@ -1,7 +1,6 @@ import { faPlus as plusIcon, faServer as serverIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Link } from 'react-router'; -import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'; +import { Dropdown, NavBar } from '@shlinkio/shlink-frontend-kit/tailwind'; import type { SelectedServer, ServersMap } from './data'; import { getServerId } from './data'; @@ -14,29 +13,28 @@ export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProp const serversList = Object.values(servers); return ( - - - Servers - - - {serversList.length === 0 ? ( - - Add a server - - ) : ( - <> - {serversList.map(({ name, id }) => ( - - {name} - - ))} - - - Manage servers - - - )} - - + + Servers + + )}> + {serversList.length === 0 ? ( + + Add a server + + ) : ( + <> + {serversList.map(({ name, id }) => ( + + {name} + + ))} + + + Manage servers + + + )} + ); }; diff --git a/src/tailwind.css b/src/tailwind.css index acb00729..e92ade35 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -8,5 +8,7 @@ :root { --footer-height: 2.3rem; --footer-margin: .8rem; + /* Temp alias fo header-height to tw-header-height, so that shlink-web-component uses the right value */ + --header-height: var(--tw-header-height); } } diff --git a/test/common/MainHeader.test.tsx b/test/common/MainHeader.test.tsx index 3832ff6f..2a102543 100644 --- a/test/common/MainHeader.test.tsx +++ b/test/common/MainHeader.test.tsx @@ -1,4 +1,4 @@ -import { screen, waitFor } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import { fromPartial } from '@total-typescript/shoehorn'; import { createMemoryHistory } from 'history'; import { Router } from 'react-router'; @@ -8,8 +8,8 @@ import { renderWithEvents } from '../__helpers__/setUpTest'; describe('', () => { const MainHeader = MainHeaderFactory(fromPartial({ - // Fake this component as a li, as it gets rendered inside a ul - ServersDropdown: () =>
  • ServersDropdown
  • , + // Fake this component as a li[role="menuitem"], as it gets rendered inside a ul[role="menu"] + ServersDropdown: () =>
  • ServersDropdown
  • , })); const setUp = (pathname = '') => { const history = createMemoryHistory(); @@ -37,35 +37,8 @@ describe('', () => { ['/settings/bar', true], ])('sets link to settings as active only when current path is settings', (currentPath, isActive) => { setUp(currentPath); - - if (isActive) { - expect(screen.getByText(/Settings$/).getAttribute('class')).toContain('active'); - } else { - expect(screen.getByText(/Settings$/).getAttribute('class')).not.toContain('active'); - } - }); - - it('renders expected class based on the nav bar state', async () => { - const { user } = setUp(); - - const toggle = screen.getByLabelText('Toggle navigation'); - const icon = toggle.firstChild; - - expect(icon).not.toHaveClass('tw:rotate-180'); - await user.click(toggle); - - expect(icon).toHaveClass('tw:rotate-180'); - await user.click(toggle); - expect(icon).not.toHaveClass('tw:rotate-180'); - }); - - it('opens Collapse when clicking toggle', async () => { - const { container, user } = setUp(); - const collapse = container.querySelector('.collapse'); - const toggle = screen.getByLabelText('Toggle navigation'); - - expect(collapse).not.toHaveAttribute('class', expect.stringContaining('show')); - await user.click(toggle); - await waitFor(() => expect(collapse).toHaveAttribute('class', expect.stringContaining('show'))); + expect(screen.getByRole('menuitem', { name: /Settings$/ })).toHaveAttribute( + 'data-active', isActive ? 'true' : 'false', + ); }); }); diff --git a/test/servers/ServersDropdown.test.tsx b/test/servers/ServersDropdown.test.tsx index a057a03d..410c2443 100644 --- a/test/servers/ServersDropdown.test.tsx +++ b/test/servers/ServersDropdown.test.tsx @@ -14,7 +14,7 @@ describe('', () => { }; const setUp = (servers: ServersMap = fallbackServers) => renderWithEvents( -
      +
      , @@ -33,16 +33,18 @@ describe('', () => { await user.click(screen.getByText('Servers')); const items = screen.getAllByRole('menuitem'); - expect(items).toHaveLength(Object.values(fallbackServers).length + 1); - expect(items[0]).toHaveTextContent('foo'); - expect(items[1]).toHaveTextContent('bar'); - expect(items[2]).toHaveTextContent('baz'); - expect(items[3]).toHaveTextContent('Manage servers'); + + // We have to add two for the "Manage servers" and the "Settings" menu items + expect(items).toHaveLength(Object.values(fallbackServers).length + 2); + expect(items[1]).toHaveTextContent('foo'); + expect(items[2]).toHaveTextContent('bar'); + expect(items[3]).toHaveTextContent('baz'); + expect(items[4]).toHaveTextContent('Manage servers'); }); it('contains a toggle with proper text', () => { setUp(); - expect(screen.getByRole('link')).toHaveTextContent('Servers'); + expect(screen.getByRole('button')).toHaveTextContent('Servers'); }); it('contains a button to manage servers', async () => { @@ -56,6 +58,6 @@ describe('', () => { const { user } = setUp({}); await user.click(screen.getByText('Servers')); - expect(screen.getByRole('menuitem')).toHaveTextContent('Add a server'); + expect(screen.getByRole('menuitem', { name: 'Add a server' })).toBeInTheDocument(); }); }); From b4db7fdf11737969199302f149c79a7fcb94c4db Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 16 Jun 2025 11:18:53 +0200 Subject: [PATCH 2/2] Remove direct dependency in reactstrap --- package-lock.json | 23 +++++++++++++++++------ package.json | 1 - 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 76384967..0fb506c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,6 @@ "react-external-link": "^2.5.0", "react-redux": "^9.2.0", "react-router": "^7.6.2", - "reactstrap": "^9.2.3", "redux-localstorage-simple": "^2.5.1", "workbox-core": "^7.3.0", "workbox-expiration": "^7.3.0", @@ -3073,6 +3072,7 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -5492,7 +5492,8 @@ }, "node_modules/classnames": { "version": "2.3.2", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/clsx": { "version": "2.1.1", @@ -9315,7 +9316,8 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-is": { "version": "16.13.1", @@ -9419,6 +9421,7 @@ "version": "9.2.3", "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-9.2.3.tgz", "integrity": "sha512-1nXy7FIBIoOgXr3AIHOpgzcZXdj6rZE5YvNSPd1hYgwv8X64m6TAJsU0ExlieJdlRXhaRfTYRSZoTWa127b0gw==", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", "@popperjs/core": "^2.6.0", @@ -9437,6 +9440,7 @@ "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", "license": "MIT", + "peer": true, "dependencies": { "react-fast-compare": "^3.0.1", "warning": "^4.0.2" @@ -11308,6 +11312,7 @@ "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.0.0" } @@ -13837,7 +13842,8 @@ "@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true }, "@reduxjs/toolkit": { "version": "2.8.2", @@ -15357,7 +15363,8 @@ "dev": true }, "classnames": { - "version": "2.3.2" + "version": "2.3.2", + "peer": true }, "clsx": { "version": "2.1.1", @@ -17955,7 +17962,8 @@ "react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "peer": true }, "react-is": { "version": "16.13.1" @@ -18015,6 +18023,7 @@ "version": "9.2.3", "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-9.2.3.tgz", "integrity": "sha512-1nXy7FIBIoOgXr3AIHOpgzcZXdj6rZE5YvNSPd1hYgwv8X64m6TAJsU0ExlieJdlRXhaRfTYRSZoTWa127b0gw==", + "peer": true, "requires": { "@babel/runtime": "^7.12.5", "@popperjs/core": "^2.6.0", @@ -18028,6 +18037,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "peer": true, "requires": { "react-fast-compare": "^3.0.1", "warning": "^4.0.2" @@ -19267,6 +19277,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "peer": true, "requires": { "loose-envify": "^1.0.0" } diff --git a/package.json b/package.json index 21d6e481..236b227b 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "react-external-link": "^2.5.0", "react-redux": "^9.2.0", "react-router": "^7.6.2", - "reactstrap": "^9.2.3", "redux-localstorage-simple": "^2.5.1", "workbox-core": "^7.3.0", "workbox-expiration": "^7.3.0",