mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-12-11 02:09:53 -06:00
Replace reactstrap nav bar with tailwind-based one
This commit is contained in:
parent
9c0c2fc3f9
commit
d10d7fd96d
@ -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*
|
||||
|
||||
50
package-lock.json
generated
50
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -64,7 +64,7 @@ const App: FCWithDeps<AppProps, AppDeps> = (
|
||||
<div className="tw:h-full">
|
||||
<MainHeader />
|
||||
|
||||
<div className="tw:h-full tw:pt-(--header-height)">
|
||||
<div className="tw:h-full tw:pt-(--tw-header-height)">
|
||||
<div
|
||||
data-testid="shlink-wrapper"
|
||||
className={clsx(
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import { faChevronDown as arrowIcon, faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCogs as cogsIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useToggle } from '@shlinkio/shlink-frontend-kit';
|
||||
import { clsx } from 'clsx';
|
||||
import { NavBar } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||
import type { FC } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router';
|
||||
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
||||
import type { FCWithDeps } from '../container/utils';
|
||||
import { componentFactory, useDependencies } from '../container/utils';
|
||||
import { ShlinkLogo } from './img/ShlinkLogo';
|
||||
@ -16,39 +13,28 @@ type MainHeaderDeps = {
|
||||
|
||||
const MainHeader: FCWithDeps<unknown, MainHeaderDeps> = () => {
|
||||
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 (
|
||||
<Navbar color="primary" dark fixed="top" expand="md" className="tw:text-white tw:bg-lm-main tw:dark:bg-dm-main">
|
||||
<NavbarBrand tag={Link} to="/">
|
||||
<ShlinkLogo className="tw:inline tw:w-7 tw:mr-1" color="white" /> Shlink
|
||||
</NavbarBrand>
|
||||
|
||||
<NavbarToggler onClick={toggleCollapse}>
|
||||
<FontAwesomeIcon
|
||||
icon={arrowIcon}
|
||||
className={clsx('tw:transition-transform tw:duration-300', { 'tw:rotate-180': isNotCollapsed })}
|
||||
/>
|
||||
</NavbarToggler>
|
||||
|
||||
<Collapse navbar isOpen={isNotCollapsed}>
|
||||
<Nav navbar className="tw:ml-auto">
|
||||
<NavItem>
|
||||
<NavLink tag={Link} to={settingsPath} active={pathname.startsWith(settingsPath)}>
|
||||
<FontAwesomeIcon icon={cogsIcon} /> Settings
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<ServersDropdown />
|
||||
</Nav>
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
<NavBar
|
||||
className="tw:[&]:fixed tw:top-0 tw:z-900"
|
||||
brand={(
|
||||
<Link to="/" className="tw:[&]:text-white tw:no-underline tw:flex tw:items-center tw:gap-2">
|
||||
<ShlinkLogo className="tw:w-7" color="white" /> <small className="tw:font-normal">Shlink</small>
|
||||
</Link>
|
||||
)}
|
||||
>
|
||||
<NavBar.MenuItem
|
||||
to={settingsPath}
|
||||
active={pathname.startsWith(settingsPath)}
|
||||
className="tw:flex tw:items-center tw:gap-1.5"
|
||||
>
|
||||
<FontAwesomeIcon icon={cogsIcon} /> Settings
|
||||
</NavBar.MenuItem>
|
||||
<ServersDropdown />
|
||||
</NavBar>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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 (
|
||||
<UncontrolledDropdown nav inNavbar>
|
||||
<DropdownToggle nav caret>
|
||||
<FontAwesomeIcon icon={serverIcon} /> <span className="tw:ml-1">Servers</span>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu end className="tw:right-0">
|
||||
{serversList.length === 0 ? (
|
||||
<DropdownItem tag={Link} to="/server/create">
|
||||
<FontAwesomeIcon icon={plusIcon} /> <span className="tw:ml-1">Add a server</span>
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<>
|
||||
{serversList.map(({ name, id }) => (
|
||||
<DropdownItem key={id} tag={Link} to={`/server/${id}`} active={getServerId(selectedServer) === id}>
|
||||
{name}
|
||||
</DropdownItem>
|
||||
))}
|
||||
<DropdownItem divider tag="hr" />
|
||||
<DropdownItem tag={Link} to="/manage-servers">
|
||||
<FontAwesomeIcon icon={serverIcon} /> <span className="tw:ml-1">Manage servers</span>
|
||||
</DropdownItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
<NavBar.Dropdown buttonContent={(
|
||||
<span className="tw:flex tw:items-center tw:gap-1.5">
|
||||
<FontAwesomeIcon icon={serverIcon} fixedWidth /> Servers
|
||||
</span>
|
||||
)}>
|
||||
{serversList.length === 0 ? (
|
||||
<Dropdown.Item to="/server/create">
|
||||
<FontAwesomeIcon icon={plusIcon} /> Add a server
|
||||
</Dropdown.Item>
|
||||
) : (
|
||||
<>
|
||||
{serversList.map(({ name, id }) => (
|
||||
<Dropdown.Item key={id} to={`/server/${id}`} selected={getServerId(selectedServer) === id}>
|
||||
{name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
<Dropdown.Separator />
|
||||
<Dropdown.Item to="/manage-servers">
|
||||
<FontAwesomeIcon icon={serverIcon} /> Manage servers
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
)}
|
||||
</NavBar.Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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('<MainHeader />', () => {
|
||||
const MainHeader = MainHeaderFactory(fromPartial({
|
||||
// Fake this component as a li, as it gets rendered inside a ul
|
||||
ServersDropdown: () => <li>ServersDropdown</li>,
|
||||
// Fake this component as a li[role="menuitem"], as it gets rendered inside a ul[role="menu"]
|
||||
ServersDropdown: () => <li role="menuitem">ServersDropdown</li>,
|
||||
}));
|
||||
const setUp = (pathname = '') => {
|
||||
const history = createMemoryHistory();
|
||||
@ -37,35 +37,8 @@ describe('<MainHeader />', () => {
|
||||
['/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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,7 +14,7 @@ describe('<ServersDropdown />', () => {
|
||||
};
|
||||
const setUp = (servers: ServersMap = fallbackServers) => renderWithEvents(
|
||||
<MemoryRouter>
|
||||
<ul>
|
||||
<ul role="menu">
|
||||
<ServersDropdown servers={servers} selectedServer={null} />
|
||||
</ul>
|
||||
</MemoryRouter>,
|
||||
@ -33,16 +33,18 @@ describe('<ServersDropdown />', () => {
|
||||
|
||||
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('<ServersDropdown />', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user