Migrate ServersListGroup to tailwind components

This commit is contained in:
Alejandro Celaya
2025-04-03 08:59:06 +02:00
parent fd7bfd845e
commit aefe5e0848
7 changed files with 62 additions and 97 deletions

View File

@@ -45,7 +45,7 @@ export const Home = ({ servers }: HomeProps) => {
>
Welcome!
</h1>
<ServersListGroup embedded servers={serversList}>
<ServersListGroup servers={serversList}>
{!hasServers && (
<div className="tw:p-6 tw:text-center tw:flex tw:flex-col tw:gap-12">
<p>This application will help you manage your Shlink servers.</p>

View File

@@ -1,49 +0,0 @@
@use '../../node_modules/@shlinkio/shlink-frontend-kit/dist/base';
@use '../utils/mixins/vertical-align';
@use '../utils/mixins/thin-scroll';
.servers-list__list-group.servers-list__list-group {
width: 100%;
}
.servers-list__list-group:not(.servers-list__list-group--embedded) {
max-width: 400px;
box-shadow: 0 .125rem .25rem rgb(0 0 0 / .075);
}
.servers-list__server-item.servers-list__server-item {
text-align: left;
position: relative;
padding: .75rem 2.5rem .75rem 1rem;
}
.servers-list__server-item:not(:hover) {
color: base.$mainColor;
}
.servers-list__server-item:hover {
background-color: var(--secondary-color);
}
.servers-list__server-item-icon {
@include vertical-align.vertical-align();
right: 1rem;
}
.servers-list__list-group--embedded.servers-list__list-group--embedded {
border-radius: 0;
border-top: 1px solid var(--border-color);
@media (min-width: base.$mdMin) {
max-height: 220px;
overflow-x: auto;
@include thin-scroll.thin-scroll();
}
.servers-list__server-item {
border: none;
border-bottom: 1px solid var(--border-color);
}
}

View File

@@ -3,33 +3,42 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { clsx } from 'clsx';
import type { FC, PropsWithChildren } from 'react';
import { Link } from 'react-router';
import { ListGroup, ListGroupItem } from 'reactstrap';
import type { ServerWithId } from './data';
import './ServersListGroup.scss';
type ServersListGroupProps = PropsWithChildren<{
servers: ServerWithId[];
embedded?: boolean;
borderless?: boolean;
}>;
const ServerListItem = ({ id, name }: { id: string; name: string }) => (
<ListGroupItem tag={Link} to={`/server/${id}`} className="servers-list__server-item">
{name}
<FontAwesomeIcon icon={chevronIcon} className="servers-list__server-item-icon" />
</ListGroupItem>
<Link
to={`/server/${id}`}
className={clsx(
'servers-list__server-item',
'tw:flex tw:items-center tw:justify-between tw:gap-x-2 tw:px-4 tw:py-3',
'tw:rounded-none tw:hover:bg-lm-secondary tw:hover:dark:bg-dm-secondary',
'tw:border-b tw:last:border-0 tw:border-lm-border tw:dark:border-dm-border',
)}
>
<span className="tw:truncate">{name}</span>
<FontAwesomeIcon icon={chevronIcon} />
</Link>
);
export const ServersListGroup: FC<ServersListGroupProps> = ({ servers, children, embedded = false }) => (
export const ServersListGroup: FC<ServersListGroupProps> = ({ servers, children, borderless }) => (
<>
{children && <div data-testid="title" className="mb-0 fs-5 fw-normal lh-sm">{children}</div>}
{children && <div data-testid="title" className="fs-5 fw-normal lh-sm">{children}</div>}
{servers.length > 0 && (
<ListGroup
<div
data-testid="list"
tag="div"
className={clsx('servers-list__list-group', { 'servers-list__list-group--embedded': embedded })}
className={clsx(
'tw:w-full tw:border-lm-border tw:dark:border-dm-border',
'tw:md:max-h-56 tw:md:overflow-y-auto tw:-mb-1 tw:scroll-thin',
{ 'tw:border-y': !borderless },
)}
>
{servers.map(({ id, name }) => <ServerListItem key={id} id={id} name={name} />)}
</ListGroup>
</div>
)}
</>
);

View File

@@ -1,4 +1,4 @@
import { Message } from '@shlinkio/shlink-frontend-kit/tailwind';
import { Card, Message } from '@shlinkio/shlink-frontend-kit/tailwind';
import type { FC } from 'react';
import { Link } from 'react-router';
import { NoMenuLayout } from '../../common/NoMenuLayout';
@@ -35,18 +35,20 @@ const ServerError: FCWithDeps<ServerErrorProps, ServerErrorDeps> = ({ servers, s
)}
</Message>
<ServersListGroup servers={Object.values(servers)}>
<p className="mb-md-3">
These are the Shlink servers currently configured. Choose one of
them or <Link to="/server/create">add a new one</Link>.
</p>
</ServersListGroup>
<p className="tw:text-xl">
These are the Shlink servers currently configured. Choose one of
them or <Link to="/server/create">add a new one</Link>.
</p>
<Card className="tw:w-full tw:max-w-100 tw:overflow-hidden tw:mt-4">
<ServersListGroup borderless servers={Object.values(servers)} />
</Card>
{isServerWithId(selectedServer) && (
<div className="container mt-3 mt-md-5">
<p className="fs-5 fw-normal lh-sm">
<p className="tw:text-xl">
Alternatively, if you think you may have misconfigured this server, you
can <DeleteServerButton server={selectedServer} className="server-error__delete-btn">remove it</DeleteServerButton> or&nbsp;
can <DeleteServerButton server={selectedServer} className="server-error__delete-btn">remove
it</DeleteServerButton> or&nbsp;
<Link to={`/server/${selectedServer.id}/edit?reconnect=true`}>edit it</Link>.
</p>
</div>

View File

@@ -1,3 +1,16 @@
@import 'tailwindcss' prefix(tw) important;
@source '../node_modules/@shlinkio/shlink-frontend-kit';
@import '@shlinkio/shlink-frontend-kit/tailwind.preset.css';
@utility scroll-thin {
/* Standard. New browsers */
scrollbar-width: thin;
/* Fallback */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
border-radius: .5rem;
}
}

View File

@@ -1,16 +0,0 @@
@mixin thin-scroll() {
/* Forefox scrollbar */
scrollbar-color: rgba(0, 0, 0, .2) #f5f5f5;
scrollbar-width: thin;
/* Chrome webkit scrollbar */
&::-webkit-scrollbar {
width: 6px;
background-color: #f5f5f5;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, .2);
border-radius: .5rem;
}
}

View File

@@ -10,12 +10,12 @@ describe('<ServersListGroup />', () => {
fromPartial({ name: 'foo', id: '123' }),
fromPartial({ name: 'bar', id: '456' }),
];
const setUp = (params: { servers?: ServerWithId[]; withChildren?: boolean; embedded?: boolean } = {}) => {
const { servers = [], withChildren = true, embedded } = params;
const setUp = (params: { servers?: ServerWithId[]; withChildren?: boolean; borderless?: boolean } = {}) => {
const { servers = [], withChildren = true, borderless } = params;
return render(
<MemoryRouter>
<ServersListGroup servers={servers} embedded={embedded}>
<ServersListGroup servers={servers} borderless={borderless}>
{withChildren ? 'The list of servers' : undefined}
</ServersListGroup>
</MemoryRouter>,
@@ -45,11 +45,17 @@ describe('<ServersListGroup />', () => {
});
it.each([
[true, 'servers-list__list-group servers-list__list-group--embedded'],
[false, 'servers-list__list-group'],
[undefined, 'servers-list__list-group'],
])('renders proper classes for embedded', (embedded, expectedClasses) => {
setUp({ servers, embedded });
expect(screen.getByTestId('list')).toHaveAttribute('class', `${expectedClasses} list-group`);
[true],
[false],
[undefined],
])('renders proper classes for embedded', (borderless) => {
setUp({ servers, borderless });
const list = screen.getByTestId('list');
if (!borderless) {
expect(list).toHaveClass('tw:border-y');
} else {
expect(list).not.toHaveClass('tw:border-y');
}
});
});