Merge pull request #1594 from shlinkio/develop

Release 4.4.1
This commit is contained in:
Alejandro Celaya
2025-06-23 10:10:47 +02:00
committed by GitHub
82 changed files with 2110 additions and 1693 deletions

View File

@@ -1,24 +0,0 @@
title: 'Q&A'
body:
- type: input
validations:
required: true
attributes:
label: shlink-web-client version
placeholder: x.y.z
- type: dropdown
validations:
required: true
attributes:
label: How do you use shlink-web-client
options:
- https://app.shlink.io
- Docker image
- Self-hosted
- Other (explain in summary)
- type: textarea
validations:
required: true
attributes:
label: Summary
value: '<!-- Describe your issue, question or request here. -->'

View File

@@ -1,7 +0,0 @@
<!--
Before opening an issue, just take into account that this is a completely free of charge and open source project.
I'm always happy to help and provide support, but some understanding will be expected.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
You may also be asked to provide tests or ways to reproduce reported bugs.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
-->

View File

@@ -2,4 +2,4 @@ blank_issues_enabled: true
contact_links:
- name: Question - Support
about: Do you need help setting up or using shlink-web-client?
url: https://github.com/shlinkio/shlink-web-client/discussions/new?category=q-a
url: https://github.com/orgs/shlinkio/discussions/new?category=help-wanted

View File

@@ -45,10 +45,6 @@ updates:
patterns:
- 'tailwindcss'
- '@tailwindcss/*'
ignore:
# Bootstrap can introduce visual breaking changes on styles
# Ignore it, since the plan is to remove it anyway
- dependency-name: 'bootstrap'
- package-ecosystem: docker
directory: '/'
schedule:

View File

@@ -4,6 +4,26 @@ 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).
## [4.4.1] - 2025-06-23
### Added
* *Nothing*
### Changed
* [shlink-web-component#661](https://github.com/shlinkio/shlink-web-component/issues/661) and [#1571](https://github.com/shlinkio/shlink-web-client/issues/1571) Fully replace bootstrap with tailwind.
* Add the new light theme brand color.
* Update to `@shlinkio/shlink-frontend-kit` 1.0.0 and `@shlinkio/shlink-web-component` 0.15
* Replace reactstrap nav bar with `NavBar` component from `@shlinkio/shlink-frontend-kit`
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* *Nothing*
## [4.4.0] - 2025-04-20
### Added
* [#1510](https://github.com/shlinkio/shlink-web-client/issues/1510) Existing HTTP credentials (cookies, TLS certs, authentication headers) can now be forwarded to the API server if appropriate [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Credentials) are set

View File

@@ -1,4 +1,4 @@
FROM node:23.11-alpine AS node
FROM node:24.2-alpine AS node
COPY . /shlink-web-client
ARG VERSION="latest"
ENV VERSION=${VERSION}

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/playwright:v1.52.0-noble
FROM mcr.microsoft.com/playwright:v1.53.1-noble
ENV NODE_VERSION 22.14
ENV TINI_VERSION v0.19.0

View File

@@ -5,7 +5,8 @@ services:
build:
context: .
dockerfile: ./dev.Dockerfile
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
working_dir: /home/shlink/www
command: /bin/sh -c "npm install && npm run start"
volumes:
- ./:/home/shlink/www
ports:

View File

@@ -84,7 +84,7 @@
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<div id="root" class="h-full"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

3155
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,23 +27,21 @@
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@json2csv/plainjs": "^7.0.6",
"@reduxjs/toolkit": "^2.7.0",
"@reduxjs/toolkit": "^2.8.2",
"@shlinkio/data-manipulation": "^1.0.3",
"@shlinkio/shlink-frontend-kit": "^0.8.12",
"@shlinkio/shlink-frontend-kit": "^1.0.0",
"@shlinkio/shlink-js-sdk": "^2.1.0",
"@shlinkio/shlink-web-component": "^0.13.3",
"bootstrap": "5.2.3",
"@shlinkio/shlink-web-component": "^0.15.0",
"bottlejs": "^2.0.1",
"clsx": "^2.1.1",
"compare-versions": "^6.1.1",
"csvtojson": "^2.0.10",
"date-fns": "^4.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-external-link": "^2.5.0",
"react-redux": "^9.2.0",
"react-router": "^7.5.1",
"reactstrap": "^9.2.3",
"react-router": "^7.6.2",
"redux-localstorage-simple": "^2.5.1",
"workbox-core": "^7.3.0",
"workbox-expiration": "^7.3.0",
@@ -53,34 +51,33 @@
},
"devDependencies": {
"@shlinkio/eslint-config-js-coding-standard": "~3.5.0",
"@stylistic/eslint-plugin": "^4.2.0",
"@tailwindcss/vite": "^4.1.4",
"@stylistic/eslint-plugin": "^4.4.1",
"@tailwindcss/vite": "^4.1.10",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@total-typescript/shoehorn": "^0.1.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.4.0",
"@vitest/browser": "^3.1.1",
"@vitest/coverage-v8": "^3.1.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.5.2",
"@vitest/browser": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4",
"adm-zip": "^0.5.16",
"axe-core": "^4.10.3",
"chalk": "^5.4.1",
"eslint": "^9.25.0",
"eslint-plugin-import": "^2.31.0",
"eslint": "^9.29.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.0.0-beta-ebf51a3-20250411",
"eslint-plugin-react-compiler": "^19.0.0-beta-714736e-20250131",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"history": "^5.3.0",
"playwright": "^1.52.0",
"sass": "^1.86.3",
"playwright": "^1.53.1",
"tailwindcss": "^4.1.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.2",
"typescript-eslint": "^8.34.1",
"vite": "^6.3.5",
"vite-plugin-pwa": "^1.0.0",
"vitest": "^3.0.5"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 B

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1 +1,8 @@
<svg width="512pt" height="512pt" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg"><g fill="#4595e3"><path d=" M 23.71 85.08 C 17.22 49.81 49.44 14.86 85.08 18.12 C 118.83 19.21 145.72 53.33 139.45 86.37 C 155.64 102.30 171.32 118.83 187.87 134.36 C 198.32 111.73 208.84 89.12 219.57 66.62 C 226.05 53.84 243.47 48.74 255.73 56.27 C 263.76 62.10 270.34 69.69 277.25 76.75 C 286.28 86.61 285.72 102.89 276.31 112.31 C 223.38 165.37 170.38 218.37 117.35 271.34 C 107.72 280.99 91.01 281.25 81.11 271.86 C 74.39 264.94 66.82 258.69 61.24 250.77 C 53.72 238.52 58.85 221.07 71.64 214.62 C 94.11 203.87 116.72 193.38 139.33 182.91 C 123.81 166.36 107.30 150.68 91.37 134.49 C 60.20 140.28 27.37 116.78 23.71 85.08 Z" /><path d=" M 205.21 201.23 C 225.32 181.36 260.88 181.11 281.14 200.86 C 299.25 218.75 317.37 236.65 335.10 254.93 C 356.73 278.01 352.01 318.70 326.03 336.56 C 320.07 330.47 313.73 324.65 308.12 318.28 C 323.86 309.39 328.76 286.18 316.63 272.39 C 301.73 256.95 286.30 242.03 271.24 226.75 C 264.49 219.65 256.80 212.00 246.37 211.52 C 224.65 208.64 205.52 233.36 214.49 253.58 C 221.09 266.81 234.22 275.12 243.62 286.24 C 240.43 295.96 238.09 306.13 238.29 316.46 C 225.55 304.29 213.16 291.73 200.89 279.09 C 180.97 257.57 183.10 220.45 205.21 201.23 Z" /><path d=" M 273.90 352.07 C 252.28 328.99 256.98 288.31 282.96 270.46 C 288.93 276.54 295.26 282.36 300.88 288.72 C 285.14 297.62 280.23 320.82 292.38 334.61 C 307.27 350.05 322.70 364.96 337.75 380.25 C 344.51 387.35 352.20 395.00 362.64 395.48 C 384.35 398.37 403.49 373.64 394.51 353.42 C 387.92 340.18 374.78 331.88 365.38 320.76 C 368.56 311.04 370.91 300.86 370.71 290.54 C 383.45 302.70 395.84 315.27 408.11 327.91 C 428.03 349.43 425.90 386.55 403.78 405.77 C 383.68 425.64 348.13 425.89 327.86 406.14 C 309.75 388.25 291.60 370.37 273.90 352.07 Z" /><path d=" M 422.11 403.83 C 431.96 394.07 441.60 384.06 451.66 374.51 C 460.90 383.74 471.89 392.70 474.89 406.11 C 480.16 429.97 484.08 454.13 488.76 478.12 C 490.00 483.41 484.47 488.29 479.35 486.63 C 454.66 481.52 429.55 478.12 405.14 471.84 C 393.17 467.97 385.20 457.75 376.55 449.27 C 386.39 439.49 396.13 429.60 406.06 419.91 C 416.37 433.45 435.74 414.00 422.11 403.83 Z" /></g></svg>
<svg width="512pt" height="512pt" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g fill="#2078CF">
<path d=" M 23.71 85.08 C 17.22 49.81 49.44 14.86 85.08 18.12 C 118.83 19.21 145.72 53.33 139.45 86.37 C 155.64 102.30 171.32 118.83 187.87 134.36 C 198.32 111.73 208.84 89.12 219.57 66.62 C 226.05 53.84 243.47 48.74 255.73 56.27 C 263.76 62.10 270.34 69.69 277.25 76.75 C 286.28 86.61 285.72 102.89 276.31 112.31 C 223.38 165.37 170.38 218.37 117.35 271.34 C 107.72 280.99 91.01 281.25 81.11 271.86 C 74.39 264.94 66.82 258.69 61.24 250.77 C 53.72 238.52 58.85 221.07 71.64 214.62 C 94.11 203.87 116.72 193.38 139.33 182.91 C 123.81 166.36 107.30 150.68 91.37 134.49 C 60.20 140.28 27.37 116.78 23.71 85.08 Z"/>
<path d=" M 205.21 201.23 C 225.32 181.36 260.88 181.11 281.14 200.86 C 299.25 218.75 317.37 236.65 335.10 254.93 C 356.73 278.01 352.01 318.70 326.03 336.56 C 320.07 330.47 313.73 324.65 308.12 318.28 C 323.86 309.39 328.76 286.18 316.63 272.39 C 301.73 256.95 286.30 242.03 271.24 226.75 C 264.49 219.65 256.80 212.00 246.37 211.52 C 224.65 208.64 205.52 233.36 214.49 253.58 C 221.09 266.81 234.22 275.12 243.62 286.24 C 240.43 295.96 238.09 306.13 238.29 316.46 C 225.55 304.29 213.16 291.73 200.89 279.09 C 180.97 257.57 183.10 220.45 205.21 201.23 Z"/>
<path d=" M 273.90 352.07 C 252.28 328.99 256.98 288.31 282.96 270.46 C 288.93 276.54 295.26 282.36 300.88 288.72 C 285.14 297.62 280.23 320.82 292.38 334.61 C 307.27 350.05 322.70 364.96 337.75 380.25 C 344.51 387.35 352.20 395.00 362.64 395.48 C 384.35 398.37 403.49 373.64 394.51 353.42 C 387.92 340.18 374.78 331.88 365.38 320.76 C 368.56 311.04 370.91 300.86 370.71 290.54 C 383.45 302.70 395.84 315.27 408.11 327.91 C 428.03 349.43 425.90 386.55 403.78 405.77 C 383.68 425.64 348.13 425.89 327.86 406.14 C 309.75 388.25 291.60 370.37 273.90 352.07 Z"/>
<path d=" M 422.11 403.83 C 431.96 394.07 441.60 384.06 451.66 374.51 C 460.90 383.74 471.89 392.70 474.89 406.11 C 480.16 429.97 484.08 454.13 488.76 478.12 C 490.00 483.41 484.47 488.29 479.35 486.63 C 454.66 481.52 429.55 478.12 405.14 471.84 C 393.17 467.97 385.20 457.75 376.55 449.27 C 386.39 439.49 396.13 429.60 406.06 419.91 C 416.37 433.45 435.74 414.00 422.11 403.83 Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 B

After

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 437 B

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 B

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 B

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 834 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 B

After

Width:  |  Height:  |  Size: 908 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 B

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 984 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,23 +1,20 @@
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
import chalk from 'chalk';
import AdmZip from 'adm-zip';
import fs from 'fs';
function zipDist(version) {
const versionFileName = `./dist/shlink-web-client_${version}_dist.zip`;
const fileBaseName = `shlink-web-client_${version}_dist`;
const versionFileName = `./dist/${fileBaseName}.zip`;
console.log(chalk.cyan(`Generating dist file for version ${chalk.bold(version)}...`));
const zip = new AdmZip();
try {
if (fs.existsSync(versionFileName)) {
fs.unlink(versionFileName);
fs.unlinkSync(versionFileName);
}
zip.addLocalFolder('./build', `shlink-web-client_${version}_dist`);
zip.addLocalFolder('./build', fileBaseName);
zip.writeZip(versionFileName);
console.log(chalk.green('Dist file properly generated'));
} catch (e) {

View File

@@ -61,15 +61,15 @@ const App: FCWithDeps<AppProps, AppDeps> = (
}, [settings.ui?.theme]);
return (
<div className="tw:px-3 tw:h-full">
<div className="h-full">
<MainHeader />
<div className="tw:h-full tw:pt-(--header-height)">
<div className="h-full pt-(--header-height)">
<div
data-testid="shlink-wrapper"
className={clsx(
'tw:min-h-full tw:pb-[calc(var(--footer-height)+var(--footer-margin))] tw:-mb-[calc(var(--footer-height)+var(--footer-margin))]',
{ 'tw:flex tw:items-center tw:pt-4': isHome },
'min-h-full pb-[calc(var(--footer-height)+var(--footer-margin))] -mb-[calc(var(--footer-height)+var(--footer-margin))]',
{ 'flex items-center pt-4': isHome },
)}
>
<Routes>
@@ -87,7 +87,7 @@ const App: FCWithDeps<AppProps, AppDeps> = (
</Routes>
</div>
<div className="tw:h-(--footer-height) tw:mt-(--footer-margin) tw:md:px-4">
<div className="h-(--footer-height) mt-(--footer-margin) md:px-4">
<ShlinkVersionsContainer />
</div>
</div>

View File

@@ -1,7 +1,6 @@
import { faSyncAlt as reloadIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useToggle } from '@shlinkio/shlink-frontend-kit';
import { Button, Card, CloseButton } from '@shlinkio/shlink-frontend-kit/tailwind';
import { Button, Card, CloseButton,useToggle } from '@shlinkio/shlink-frontend-kit';
import { clsx } from 'clsx';
import type { FC } from 'react';
import { useCallback } from 'react';
@@ -13,7 +12,7 @@ interface AppUpdateBannerProps {
}
export const AppUpdateBanner: FC<AppUpdateBannerProps> = ({ isOpen, onClose, forceUpdate }) => {
const [isUpdating,, setUpdating] = useToggle();
const { flag: isUpdating, setToTrue: setUpdating } = useToggle();
const update = useCallback(() => {
setUpdating();
forceUpdate();
@@ -27,15 +26,15 @@ export const AppUpdateBanner: FC<AppUpdateBannerProps> = ({ isOpen, onClose, for
<Card
role="alert"
className={clsx(
'tw:w-[700px] tw:max-w-[calc(100%-30px)]',
'tw:fixed tw:top-[35px] tw:left-[50%] tw:translate-x-[-50%] tw:z-[1040]',
'w-[700px] max-w-[calc(100%-30px)]',
'fixed top-[35px] left-[50%] translate-x-[-50%] z-[1040]',
)}
>
<Card.Header className="tw:flex tw:items-center tw:justify-between">
<Card.Header className="flex items-center justify-between">
<h5>This app has just been updated!</h5>
<CloseButton onClick={onClose} />
</Card.Header>
<Card.Body className="tw:flex tw:gap-4 tw:items-center tw:justify-between tw:max-md:flex-col">
<Card.Body className="flex gap-4 items-center justify-between max-md:flex-col">
Restart it to enjoy the new features.
<Button disabled={isUpdating} variant="secondary" solid onClick={update}>
{!isUpdating && <>Restart now <FontAwesomeIcon icon={reloadIcon} /></>}

View File

@@ -1,4 +1,4 @@
import { Button } from '@shlinkio/shlink-frontend-kit/tailwind';
import { Button } from '@shlinkio/shlink-frontend-kit';
import type { PropsWithChildren, ReactNode } from 'react';
import { Component } from 'react';
import { ErrorLayout } from './ErrorLayout';

View File

@@ -1,4 +1,4 @@
import { SimpleCard } from '@shlinkio/shlink-frontend-kit/tailwind';
import { SimpleCard } from '@shlinkio/shlink-frontend-kit';
import type { FC, PropsWithChildren } from 'react';
export type ErrorLayoutProps = PropsWithChildren<{
@@ -6,8 +6,8 @@ export type ErrorLayoutProps = PropsWithChildren<{
}>;
export const ErrorLayout: FC<ErrorLayoutProps> = ({ children, title }) => (
<div className="tw:pt-4">
<SimpleCard className="tw:p-4 tw:w-full tw:lg:w-[65%] tw:m-auto">
<div className="pt-4">
<SimpleCard className="p-4 w-full lg:w-[65%] m-auto">
<h2>{title}</h2>
{children}
</SimpleCard>

View File

@@ -1,6 +1,6 @@
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, Card } from '@shlinkio/shlink-frontend-kit/tailwind';
import { Button, Card } from '@shlinkio/shlink-frontend-kit';
import { clsx } from 'clsx';
import { useEffect } from 'react';
import { ExternalLink } from 'react-external-link';
@@ -27,26 +27,26 @@ export const Home = ({ servers }: HomeProps) => {
}, [serversList, navigate]);
return (
<div className="tw:w-full">
<Card className="tw:mx-auto tw:max-w-[720px] tw:overflow-hidden">
<div className="tw:flex tw:flex-col tw:md:flex-row">
<div className="tw:p-6 tw:hidden tw:md:flex tw:items-center tw:w-[40%]">
<div className="tw:w-full">
<div className="px-3 w-full">
<Card className="mx-auto max-w-[720px] overflow-hidden">
<div className="flex flex-col md:flex-row">
<div className="p-6 hidden md:flex items-center w-[40%]">
<div className="w-full">
<ShlinkLogo />
</div>
</div>
<div className="tw:md:border-l tw:border-lm-border tw:dark:border-dm-border tw:flex-grow">
<div className="md:border-l border-lm-border dark:border-dm-border flex-grow">
<h1
className={clsx(
'tw:p-4 tw:text-center tw:border-lm-border tw:dark:border-dm-border',
{ 'tw:border-b': !hasServers },
'p-4 text-center border-lm-border dark:border-dm-border',
{ 'border-b': !hasServers },
)}
>
Welcome!
</h1>
{hasServers ? <ServersListGroup servers={serversList} /> : (
<div className="tw:p-6 tw:text-center tw:flex tw:flex-col tw:gap-12 tw:text-xl">
<div className="p-6 text-center flex flex-col gap-12 text-xl">
<p>This application will help you manage your Shlink servers.</p>
<p>
<Button to="/server/create" size="lg" inline>
@@ -56,7 +56,7 @@ export const Home = ({ servers }: HomeProps) => {
<p>
<ExternalLink href="https://shlink.io/documentation">
<small>
<span className="tw:mr-2">Learn more about Shlink</span>
<span className="mr-2">Learn more about Shlink</span>
<FontAwesomeIcon icon={faExternalLinkAlt} />
</small>
</ExternalLink>

View File

@@ -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';
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 [isNotCollapsed, toggleCollapse, , collapse] = useToggle();
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-brand tw:dark:bg-dm-brand">
<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} />&nbsp; Settings
</NavLink>
</NavItem>
<ServersDropdown />
</Nav>
</Collapse>
</Navbar>
<NavBar
className="[&]:fixed top-0 z-900"
brand={(
<Link to="/" className="[&]:text-white no-underline flex items-center gap-2">
<ShlinkLogo className="w-7" color="white" /> <small className="font-normal">Shlink</small>
</Link>
)}
>
<NavBar.MenuItem
to={settingsPath}
active={pathname.startsWith(settingsPath)}
className="flex items-center gap-1.5"
>
<FontAwesomeIcon icon={cogsIcon} /> Settings
</NavBar.MenuItem>
<ServersDropdown />
</NavBar>
);
};

View File

@@ -6,7 +6,7 @@ export type NoMenuLayoutProps = PropsWithChildren & {
};
export const NoMenuLayout: FC<NoMenuLayoutProps> = ({ children, className }) => (
<div className={clsx('tw:container tw:mx-auto tw:p-5 tw:pt-8 tw:max-md:p-0 tw:max-md:py-4', className)}>
<div className={clsx('container mx-auto p-5 pt-8 max-md:p-3 max-md:py-4', className)}>
{children}
</div>
);

View File

@@ -1,4 +1,4 @@
import { Button } from '@shlinkio/shlink-frontend-kit/tailwind';
import { Button } from '@shlinkio/shlink-frontend-kit';
import type { FC, PropsWithChildren } from 'react';
import { ErrorLayout } from './ErrorLayout';

View File

@@ -12,7 +12,7 @@ export interface ShlinkVersionsProps {
}
const VersionLink = ({ project, version }: { project: 'shlink' | 'shlink-web-client'; version: string }) => (
<ExternalLink href={`https://github.com/shlinkio/${project}/releases/${version}`} className="tw:text-gray-500">
<ExternalLink href={`https://github.com/shlinkio/${project}/releases/${version}`} className="text-gray-500">
<b>{version}</b>
</ExternalLink>
);
@@ -21,7 +21,7 @@ export const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIE
const normalizedClientVersion = normalizeVersion(clientVersion);
return (
<small className="tw:text-gray-500">
<small className="text-gray-500">
{isReachableServer(selectedServer) && (
<>Server: <VersionLink project="shlink" version={selectedServer.printableVersion} /> - </>
)}

View File

@@ -9,7 +9,7 @@ export type ShlinkVersionsContainerProps = {
export const ShlinkVersionsContainer = ({ selectedServer }: ShlinkVersionsContainerProps) => (
<div
className={clsx('tw:text-center', { 'tw:md:ml-(--aside-menu-width)': isReachableServer(selectedServer) })}
className={clsx('text-center', { 'md:ml-(--aside-menu-width)': isReachableServer(selectedServer) })}
>
<ShlinkVersions selectedServer={selectedServer} />
</div>

View File

@@ -1,11 +1,11 @@
import { MAIN_COLOR } from '@shlinkio/shlink-frontend-kit';
import { brandColor } from '@shlinkio/shlink-frontend-kit';
export interface ShlinkLogoProps {
color?: string;
className?: string;
}
export const ShlinkLogo = ({ color = MAIN_COLOR, className }: ShlinkLogoProps) => (
export const ShlinkLogo = ({ color = brandColor(), className }: ShlinkLogoProps) => (
<svg className={className} viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g fill={color}>
<path

View File

@@ -1,6 +0,0 @@
@use '../node_modules/@shlinkio/shlink-frontend-kit/dist/base'; // Before bootstrap stylesheet
@use '../node_modules/bootstrap/scss/bootstrap.scss' with (
$primary: base.$mainColor // Override bootstrap's primary color
);
@use '../node_modules/@shlinkio/shlink-frontend-kit/dist/index'; // After bootstrap. Includes CSS overrides
@use '../node_modules/@shlinkio/shlink-web-component/dist/index' as c-index;

View File

@@ -6,7 +6,6 @@ import { container } from './container';
import { setUpStore } from './container/store';
import { register as registerServiceWorker } from './serviceWorkerRegistration';
import './tailwind.css';
import './index.scss';
const store = setUpStore(container);
const { App, ScrollToTop, ErrorHandler, appUpdateAvailable } = container;

View File

@@ -1,7 +1,5 @@
import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
import { useToggle } from '@shlinkio/shlink-frontend-kit';
import type { ResultProps } from '@shlinkio/shlink-frontend-kit/tailwind';
import { Button, Result } from '@shlinkio/shlink-frontend-kit/tailwind';
import type { ResultProps,TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
import { Button, Result,useToggle } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react';
import { useCallback, useState } from 'react';
import { useNavigate } from 'react-router';
@@ -28,7 +26,7 @@ type CreateServerDeps = {
};
const ImportResult = ({ variant }: Pick<ResultProps, 'variant'>) => (
<div className="tw:mt-4">
<div className="mt-4">
<Result variant={variant}>
{variant === 'success' && 'Servers properly imported. You can now select one from the list :)'}
{variant === 'error' && 'The servers could not be imported. Make sure the format is correct.'}
@@ -42,10 +40,10 @@ const CreateServer: FCWithDeps<CreateServerProps, CreateServerDeps> = ({ servers
const goBack = useGoBack();
const hasServers = !!Object.keys(servers).length;
// eslint-disable-next-line react-compiler/react-compiler
const [serversImported, setServersImported] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
const [serversImported, setServersImported] = useTimeoutToggle({ delay: SHOW_IMPORT_MSG_TIME });
// eslint-disable-next-line react-compiler/react-compiler
const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
const [isConfirmModalOpen, toggleConfirmModal] = useToggle();
const [errorImporting, setErrorImporting] = useTimeoutToggle({ delay: SHOW_IMPORT_MSG_TIME });
const { flag: isConfirmModalOpen, toggle: toggleConfirmModal } = useToggle();
const [serverData, setServerData] = useState<ServerData>();
const saveNewServer = useCallback((newServerData: ServerData) => {
const [newServerWithUniqueId] = ensureUniqueIds(servers, [newServerData]);

View File

@@ -17,7 +17,7 @@ type DeleteServerButtonDeps = {
const DeleteServerButton: FCWithDeps<DeleteServerButtonProps, DeleteServerButtonDeps> = ({ server, children }) => {
const { DeleteServerModal } = useDependencies(DeleteServerButton);
const [isModalOpen, , showModal, hideModal] = useToggle();
const { flag: isModalOpen, setToTrue: showModal, setToFalse: hideModal } = useToggle();
const navigate = useNavigate();
const onClose = useCallback((confirmed: boolean) => {
hideModal();
@@ -28,7 +28,7 @@ const DeleteServerButton: FCWithDeps<DeleteServerButtonProps, DeleteServerButton
return (
<>
<button type="button" className="tw:text-danger tw:hover:underline" onClick={showModal}>
<button type="button" className="text-danger hover:underline" onClick={showModal}>
{children}
</button>
<DeleteServerModal server={server} open={isModalOpen} onClose={onClose} />

View File

@@ -1,5 +1,5 @@
import type { ExitAction } from '@shlinkio/shlink-frontend-kit/tailwind';
import { CardModal } from '@shlinkio/shlink-frontend-kit/tailwind';
import type { ExitAction } from '@shlinkio/shlink-frontend-kit';
import { CardModal } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react';
import { useCallback } from 'react';
import type { ServerWithId } from './data';
@@ -31,7 +31,7 @@ export const DeleteServerModal: FC<DeleteServerModalConnectProps> = ({ server, o
onClosed={onClosed}
confirmText="Delete"
>
<div className="tw:flex tw:flex-col tw:gap-y-4">
<div className="flex flex-col gap-y-4">
<p>Are you sure you want to remove <b>{server ? server.name : ''}</b>?</p>
<p>
<i>

View File

@@ -1,5 +1,4 @@
import { useParsedQuery } from '@shlinkio/shlink-frontend-kit';
import { Button } from '@shlinkio/shlink-frontend-kit/tailwind';
import { Button,useParsedQuery } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react';
import { NoMenuLayout } from '../common/NoMenuLayout';
import type { FCWithDeps } from '../container/utils';

View File

@@ -1,7 +1,7 @@
import { faFileDownload as exportIcon, faPlus as plusIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
import { Button, Result, SearchInput, SimpleCard, Table } from '@shlinkio/shlink-frontend-kit/tailwind';
import { Button, Result, SearchInput, SimpleCard, Table } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react';
import { useMemo, useState } from 'react';
import { NoMenuLayout } from '../common/NoMenuLayout';
@@ -40,22 +40,22 @@ const ManageServers: FCWithDeps<ManageServersProps, ManageServersDeps> = ({ serv
);
const hasAutoConnect = allServers.some(({ autoConnect }) => !!autoConnect);
// eslint-disable-next-line react-compiler/react-compiler
const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
const [errorImporting, setErrorImporting] = useTimeoutToggle({ delay: SHOW_IMPORT_MSG_TIME });
return (
<NoMenuLayout className="tw:flex tw:flex-col tw:gap-y-4">
<NoMenuLayout className="flex flex-col gap-y-4">
<SearchInput onChange={setSearchTerm} />
<div className="tw:flex tw:flex-col tw:md:flex-row tw:gap-2">
<div className="tw:flex tw:gap-2">
<ImportServersBtn className="tw:flex-grow" onError={setErrorImporting}>Import servers</ImportServersBtn>
<div className="flex flex-col md:flex-row gap-2">
<div className="flex gap-2">
<ImportServersBtn className="flex-grow" onError={setErrorImporting}>Import servers</ImportServersBtn>
{filteredServers.length > 0 && (
<Button variant="secondary" className="tw:flex-grow" onClick={async () => serversExporter.exportServers()}>
<Button variant="secondary" className="flex-grow" onClick={async () => serversExporter.exportServers()}>
<FontAwesomeIcon icon={exportIcon} /> Export servers
</Button>
)}
</div>
<Button className="tw:md:ml-auto" to="/server/create">
<Button className="md:ml-auto" to="/server/create">
<FontAwesomeIcon icon={plusIcon} /> Add a server
</Button>
</div>
@@ -64,7 +64,7 @@ const ManageServers: FCWithDeps<ManageServersProps, ManageServersDeps> = ({ serv
<Table header={(
<Table.Row>
{hasAutoConnect && (
<Table.Cell className="tw:w-[35px]"><span className="tw:sr-only">Auto-connect</span></Table.Cell>
<Table.Cell className="w-[35px]"><span className="sr-only">Auto-connect</span></Table.Cell>
)}
<Table.Cell>Name</Table.Cell>
<Table.Cell>Base URL</Table.Cell>
@@ -72,7 +72,7 @@ const ManageServers: FCWithDeps<ManageServersProps, ManageServersDeps> = ({ serv
</Table.Row>
)}>
{!filteredServers.length && (
<Table.Row className="tw:text-center"><Table.Cell colSpan={4}>No servers found.</Table.Cell></Table.Row>
<Table.Row className="text-center"><Table.Cell colSpan={4}>No servers found.</Table.Cell></Table.Row>
)}
{filteredServers.map((server) => (
<ManageServersRow key={server.id} server={server} hasAutoConnect={hasAutoConnect} />

View File

@@ -1,9 +1,8 @@
import { faCheck as checkIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Table } from '@shlinkio/shlink-frontend-kit/tailwind';
import { Table, Tooltip, useTooltip } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react';
import { Link } from 'react-router';
import { UncontrolledTooltip } from 'reactstrap';
import type { FCWithDeps } from '../container/utils';
import { componentFactory, useDependencies } from '../container/utils';
import type { ServerWithId } from './data';
@@ -20,26 +19,29 @@ type ManageServersRowDeps = {
const ManageServersRow: FCWithDeps<ManageServersRowProps, ManageServersRowDeps> = ({ server, hasAutoConnect }) => {
const { ManageServersRowDropdown } = useDependencies(ManageServersRow);
const { anchor, tooltip } = useTooltip();
return (
<Table.Row className="tw:relative">
<Table.Row className="relative">
{hasAutoConnect && (
<Table.Cell columnName="Auto-connect">
{server.autoConnect && (
<>
<FontAwesomeIcon icon={checkIcon} className="tw:text-brand" id="autoConnectIcon" />
<UncontrolledTooltip target="autoConnectIcon" placement="right">
Auto-connect to this server
</UncontrolledTooltip>
<FontAwesomeIcon
icon={checkIcon}
className="text-lm-brand dark:text-dm-brand"
{...anchor}
/>
<Tooltip {...tooltip}>Auto-connect to this server</Tooltip>
</>
)}
</Table.Cell>
)}
<Table.Cell className="tw:font-bold" columnName="Name">
<Table.Cell className="font-bold" columnName="Name">
<Link to={`/server/${server.id}`}>{server.name}</Link>
</Table.Cell>
<Table.Cell columnName="Base URL" className="tw:max-lg:border-b-0">{server.url}</Table.Cell>
<Table.Cell className="tw:text-right tw:max-lg:absolute tw:right-0 tw:-top-1 tw:mx-lg:pt-0">
<Table.Cell columnName="Base URL" className="max-lg:border-b-0">{server.url}</Table.Cell>
<Table.Cell className="text-right max-lg:absolute right-0 -top-1 mx-lg:pt-0">
<ManageServersRowDropdown server={server} />
</Table.Cell>
</Table.Row>

View File

@@ -6,10 +6,8 @@ import {
faPlug as connectIcon,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { RowDropdownBtn, useToggle } from '@shlinkio/shlink-frontend-kit';
import { RowDropdown,useToggle } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react';
import { Link } from 'react-router';
import { DropdownItem } from 'reactstrap';
import type { FCWithDeps } from '../container/utils';
import { componentFactory, useDependencies } from '../container/utils';
import type { ServerWithId } from './data';
@@ -31,28 +29,28 @@ const ManageServersRowDropdown: FCWithDeps<ManageServersRowDropdownConnectProps,
{ server, setAutoConnect },
) => {
const { DeleteServerModal } = useDependencies(ManageServersRowDropdown);
const [isModalOpen,, showModal, hideModal] = useToggle();
const { flag: isModalOpen, setToTrue: showModal, setToFalse: hideModal } = useToggle();
const serverUrl = `/server/${server.id}`;
const { autoConnect: isAutoConnect } = server;
const autoConnectIcon = isAutoConnect ? toggleOffIcon : toggleOnIcon;
return (
<>
<RowDropdownBtn minWidth={isAutoConnect ? 210 : 170}>
<DropdownItem tag={Link} to={serverUrl}>
<RowDropdown menuAlignment="right">
<RowDropdown.Item to={serverUrl} className="gap-1.5">
<FontAwesomeIcon icon={connectIcon} fixedWidth /> Connect
</DropdownItem>
<DropdownItem tag={Link} to={`${serverUrl}/edit`}>
</RowDropdown.Item>
<RowDropdown.Item to={`${serverUrl}/edit`} className="gap-1.5">
<FontAwesomeIcon icon={editIcon} fixedWidth /> Edit server
</DropdownItem>
<DropdownItem onClick={() => setAutoConnect(server, !isAutoConnect)}>
</RowDropdown.Item>
<RowDropdown.Item onClick={() => setAutoConnect(server, !isAutoConnect)} className="gap-1.5">
<FontAwesomeIcon icon={autoConnectIcon} fixedWidth /> {isAutoConnect ? 'Do not a' : 'A'}uto-connect
</DropdownItem>
<DropdownItem divider tag="hr" />
<DropdownItem className="tw:text-danger" onClick={showModal}>
</RowDropdown.Item>
<RowDropdown.Separator />
<RowDropdown.Item className="[&]:text-danger gap-1.5" onClick={showModal}>
<FontAwesomeIcon icon={deleteIcon} fixedWidth /> Remove server
</DropdownItem>
</RowDropdownBtn>
</RowDropdown.Item>
</RowDropdown>
<DeleteServerModal server={server} open={isModalOpen} onClose={hideModal} />
</>

View File

@@ -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';
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="flex items-center 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>
);
};

View File

@@ -15,12 +15,12 @@ const ServerListItem = ({ id, name }: { id: string; name: string }) => (
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',
'flex items-center justify-between gap-x-2 px-4 py-3',
'rounded-none hover:bg-lm-secondary hover:dark:bg-dm-secondary',
'border-b last:border-0 border-lm-border dark:border-dm-border',
)}
>
<span className="tw:truncate">{name}</span>
<span className="truncate">{name}</span>
<FontAwesomeIcon icon={chevronIcon} />
</Link>
);
@@ -31,9 +31,9 @@ export const ServersListGroup: FC<ServersListGroupProps> = ({ servers, borderles
<div
data-testid="list"
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 },
'w-full border-lm-border dark:border-dm-border',
'md:max-h-56 md:overflow-y-auto -mb-1 scroll-thin',
{ 'border-y': !borderless },
)}
>
{servers.map(({ id, name }) => <ServerListItem key={id} id={id} name={name} />)}

View File

@@ -1,4 +1,4 @@
import { CardModal } from '@shlinkio/shlink-frontend-kit/tailwind';
import { CardModal } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react';
import { Fragment } from 'react';
import type { ServerData } from '../data';
@@ -26,7 +26,7 @@ export const DuplicatedServersModal: FC<DuplicatedServersModalProps> = (
cancelText={hasMultipleServers ? 'Ignore duplicates' : 'Discard'}
>
<p>{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}</p>
<ul className="tw:list-disc tw:mt-4">
<ul className="list-disc mt-4">
{duplicatedServers.map(({ url, apiKey }, index) => (!hasMultipleServers ? (
<Fragment key={index}>
<li>URL: <b>{url}</b></li>

View File

@@ -1,10 +1,8 @@
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useElementRef, useToggle } from '@shlinkio/shlink-frontend-kit';
import { Button } from '@shlinkio/shlink-frontend-kit/tailwind';
import { Button, Tooltip, useToggle , useTooltip } from '@shlinkio/shlink-frontend-kit';
import type { ChangeEvent, PropsWithChildren } from 'react';
import { useCallback, useRef , useState } from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import { useCallback, useRef, useState } from 'react';
import type { FCWithDeps } from '../../container/utils';
import { componentFactory, useDependencies } from '../../container/utils';
import type { ServerData, ServersMap, ServerWithId } from '../data';
@@ -38,9 +36,10 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
className = '',
}) => {
const { ServersImporter: serversImporter } = useDependencies(ImportServersBtn);
const ref = useElementRef<HTMLInputElement>();
const fileInputRef = useRef<HTMLInputElement>(null);
const { anchor, tooltip } = useTooltip({ placement: tooltipPlacement });
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
const [isModalOpen,, showModal, hideModal] = useToggle();
const { flag: isModalOpen, setToTrue: showModal, setToFalse: hideModal } = useToggle();
const newServersCreatedRef = useRef(false);
const onFile = useCallback(
@@ -84,20 +83,20 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
return (
<>
<Button variant="secondary" id="importBtn" className={className} onClick={() => ref.current?.click()}>
<Button variant="secondary" className={className} onClick={() => fileInputRef.current?.click()} {...anchor}>
<FontAwesomeIcon icon={importIcon} fixedWidth /> {children ?? 'Import from file'}
</Button>
<UncontrolledTooltip placement={tooltipPlacement} target="importBtn">
<Tooltip {...tooltip}>
You can create servers by importing a CSV file with <b>name</b>, <b>apiKey</b> and <b>url</b> columns.
</UncontrolledTooltip>
</Tooltip>
<input
type="file"
accept=".csv"
className="tw:hidden"
className="hidden"
aria-hidden
tabIndex={-1}
ref={ref as any /* TODO Remove After updating to React 19 */}
ref={fileInputRef}
onChange={onFile}
data-testid="csv-file-input"
/>

View File

@@ -1,4 +1,4 @@
import { Card, Message } from '@shlinkio/shlink-frontend-kit/tailwind';
import { Card, Message } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react';
import { Link } from 'react-router';
import { NoMenuLayout } from '../../common/NoMenuLayout';
@@ -23,8 +23,8 @@ const ServerError: FCWithDeps<ServerErrorProps, ServerErrorDeps> = ({ servers, s
return (
<NoMenuLayout>
<div className="tw:flex tw:flex-col tw:items-center tw:gap-y-4 tw:md:gap-y-8">
<Message className="tw:w-full tw:lg:w-[80%]" variant="error">
<div className="flex flex-col items-center gap-y-4 md:gap-y-8">
<Message className="w-full lg:w-[80%]" variant="error">
{!isServerWithId(selectedServer) && 'Could not find this Shlink server.'}
{isServerWithId(selectedServer) && (
<>
@@ -34,16 +34,16 @@ const ServerError: FCWithDeps<ServerErrorProps, ServerErrorDeps> = ({ servers, s
)}
</Message>
<p className="tw:text-xl">
<p className="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">
<Card className="w-full max-w-100 overflow-hidden">
<ServersListGroup borderless servers={Object.values(servers)} />
</Card>
{isServerWithId(selectedServer) && (
<p className="tw:text-xl">
<p className="text-xl">
Alternatively, if you think you may have misconfigured this server, you
can <DeleteServerButton server={selectedServer}>remove
it</DeleteServerButton> or&nbsp;

View File

@@ -1,4 +1,3 @@
import { useToggle } from '@shlinkio/shlink-frontend-kit';
import {
Checkbox,
Details,
@@ -6,7 +5,8 @@ import {
LabelledInput,
LabelledRevealablePasswordInput,
SimpleCard,
} from '@shlinkio/shlink-frontend-kit/tailwind';
useToggle,
} from '@shlinkio/shlink-frontend-kit';
import type { FC, PropsWithChildren, ReactNode } from 'react';
import { useState } from 'react';
import { usePreventDefault } from '../../utils/utils';
@@ -24,13 +24,12 @@ export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, child
const [apiKey, setApiKey] = useState(initialValues?.apiKey ?? '');
const { flag: forwardCredentials, toggle: toggleForwardCredentials } = useToggle(
initialValues?.forwardCredentials ?? false,
true,
);
const handleSubmit = usePreventDefault(() => onSubmit({ name, url, apiKey, forwardCredentials }));
return (
<form name="serverForm" onSubmit={handleSubmit}>
<SimpleCard className="tw:mb-4" bodyClassName="tw:flex tw:flex-col tw:gap-y-3" title={title}>
<SimpleCard className="mb-4" bodyClassName="flex flex-col gap-y-3" title={title}>
<LabelledInput label="Name" value={name} onChange={(e) => setName(e.target.value)} required />
<LabelledInput label="URL" type="url" value={url} onChange={(e) => setUrl(e.target.value)} required />
<LabelledRevealablePasswordInput
@@ -40,25 +39,25 @@ export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, child
required
/>
<Details summary="Advanced options">
<div className="tw:flex tw:flex-col tw:gap-0.5">
<Label className="tw:flex tw:items-center tw:gap-x-1.5 tw:cursor-pointer">
<div className="flex flex-col gap-0.5">
<Label className="flex items-center gap-x-1.5 cursor-pointer">
<Checkbox onChange={toggleForwardCredentials} checked={forwardCredentials} />
Forward credentials to this server on every request.
</Label>
<small className="tw:pl-5.5 tw:text-gray-600 tw:dark:text-gray-400 tw:mt-0.5">
<small className="pl-5.5 text-gray-600 dark:text-gray-400 mt-0.5">
{'"'}Credentials{'"'} here means cookies, TLS client certificates, or authentication headers containing a username
and password.
</small>
<small className="tw:pl-5.5 tw:text-gray-600 tw:dark:text-gray-400">
<small className="pl-5.5 text-gray-600 dark:text-gray-400">
<b>Important!</b> If you are not sure what this means, leave it unchecked. Enabling this option will
make all requests fail for Shlink older than v4.5.0, as it requires the server to set a more strict
value for <code className="tw:whitespace-nowrap">Access-Control-Allow-Origin</code> than <code>*</code>.
value for <code className="whitespace-nowrap">Access-Control-Allow-Origin</code> than <code>*</code>.
</small>
</div>
</Details>
</SimpleCard>
<div className="tw:flex tw:items-center tw:justify-end tw:gap-x-2">{children}</div>
<div className="flex items-center justify-end gap-x-2">{children}</div>
</form>
);
};

View File

@@ -1,4 +1,4 @@
import { Message } from '@shlinkio/shlink-frontend-kit/tailwind';
import { Message } from '@shlinkio/shlink-frontend-kit';
import type { FC } from 'react';
import { useEffect } from 'react';
import { useParams } from 'react-router';

View File

@@ -1,23 +1,14 @@
@import 'tailwindcss' prefix(tw) important;
@source '../node_modules/@shlinkio/shlink-frontend-kit';
@import 'tailwindcss';
@import '@shlinkio/shlink-frontend-kit/tailwind.preset.css';
@theme {
/* Override breakpoints with the values from bootstrap, to keep sizing until fully migrated */
--breakpoint-sm: 576px;
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
--breakpoint-2xl: 1400px;
}
@import '@shlinkio/shlink-web-component/tailwind.preset.css';
@source '../node_modules/@shlinkio/shlink-frontend-kit';
@source '../node_modules/@shlinkio/shlink-web-component';
@layer base {
:root {
--header-height: 56px;
--footer-height: 2.3rem;
--footer-margin: .8rem;
/* Width of ShlinkWebComponent's side menu when not collapsed */
--aside-menu-width: 260px;
/* FIXME Remove this once updated to shlink-web-component 0.15.1 */
--header-height: 52px;
}
}

View File

@@ -62,9 +62,9 @@ describe('<App />', () => {
const shlinkWrapper = screen.getByTestId('shlink-wrapper');
if (isFlex) {
expect(shlinkWrapper).toHaveClass('tw:flex');
expect(shlinkWrapper).toHaveClass('flex');
} else {
expect(shlinkWrapper).not.toHaveClass('tw:flex');
expect(shlinkWrapper).not.toHaveClass('flex');
}
});
});

View File

@@ -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',
);
});
});

View File

@@ -23,9 +23,9 @@ describe('<ShlinkVersionsContainer />', () => {
const { container } = setUp(selectedServer);
if (shouldAddMargin) {
expect(container.firstChild).toHaveClass('tw:md:ml-(--aside-menu-width)');
expect(container.firstChild).toHaveClass('md:ml-(--aside-menu-width)');
} else {
expect(container.firstChild).not.toHaveClass('tw:md:ml-(--aside-menu-width)');
expect(container.firstChild).not.toHaveClass('md:ml-(--aside-menu-width)');
}
});
});

View File

@@ -1,4 +1,4 @@
import { MAIN_COLOR } from '@shlinkio/shlink-frontend-kit';
import { brandColor } from '@shlinkio/shlink-frontend-kit';
import { render } from '@testing-library/react';
import type { ShlinkLogoProps } from '../../../src/common/img/ShlinkLogo';
import { ShlinkLogo } from '../../../src/common/img/ShlinkLogo';
@@ -10,7 +10,7 @@ describe('<ShlinkLogo />', () => {
it('passes a11y checks', () => checkAccessibility(setUp()));
it.each([
[undefined, MAIN_COLOR],
[undefined, brandColor()],
['red', 'red'],
['white', 'white'],
])('renders expected color', (color, expectedColor) => {

View File

@@ -1,4 +1,4 @@
import { Table } from '@shlinkio/shlink-frontend-kit/tailwind';
import { Table } from '@shlinkio/shlink-frontend-kit';
import { render, screen } from '@testing-library/react';
import { fromPartial } from '@total-typescript/shoehorn';
import { MemoryRouter } from 'react-router';

View File

@@ -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();
});
});

View File

@@ -41,9 +41,9 @@ describe('<ServersListGroup />', () => {
const list = screen.getByTestId('list');
if (!borderless) {
expect(list).toHaveClass('tw:border-y');
expect(list).toHaveClass('border-y');
} else {
expect(list).not.toHaveClass('tw:border-y');
expect(list).not.toHaveClass('border-y');
}
});
});

View File

@@ -2,7 +2,7 @@
exports[`<DeleteServerButton /> > renders expected content 1`] = `
<button
class="tw:text-danger tw:hover:underline"
class="text-danger hover:underline"
type="button"
>
Foo bar
@@ -11,7 +11,7 @@ exports[`<DeleteServerButton /> > renders expected content 1`] = `
exports[`<DeleteServerButton /> > renders expected content 2`] = `
<button
class="tw:text-danger tw:hover:underline"
class="text-danger hover:underline"
type="button"
>
baz
@@ -20,7 +20,7 @@ exports[`<DeleteServerButton /> > renders expected content 2`] = `
exports[`<DeleteServerButton /> > renders expected content 3`] = `
<button
class="tw:text-danger tw:hover:underline"
class="text-danger hover:underline"
type="button"
>
something

View File

@@ -3,32 +3,31 @@
exports[`<ManageServersRow /> > renders auto-connect icon only if server is autoConnect 1`] = `
<div>
<table
class="tw:w-full"
class="w-full"
>
<thead
class="tw:hidden tw:lg:table-header-group"
class="hidden lg:table-header-group"
>
<tr
class="tw:group tw:lg:table-row tw:flex tw:flex-col tw:lg:border-0 tw:border-y-2 tw:border-lm-border tw:dark:border-dm-border"
class="group lg:table-row flex flex-col lg:border-0 border-y-2 border-lm-border dark:border-dm-border"
/>
</thead>
<tbody
class="tw:lg:table-row-group tw:flex tw:flex-col tw:gap-y-3"
class="lg:table-row-group flex flex-col gap-y-3"
>
<tr
class="tw:group tw:lg:table-row tw:flex tw:flex-col tw:lg:border-0 tw:border-y-2 tw:border-lm-border tw:dark:border-dm-border tw:hover:bg-lm-primary tw:dark:hover:bg-dm-primary tw:group-[&]/card:hover:bg-lm-secondary tw:dark:group-[&]/card:hover:bg-dm-secondary tw:relative"
class="group lg:table-row flex flex-col lg:border-0 border-y-2 border-lm-border dark:border-dm-border hover:bg-lm-primary dark:hover:bg-dm-primary group-[&]/card:hover:bg-lm-secondary dark:group-[&]/card:hover:bg-dm-secondary relative"
>
<td
class="tw:p-2 tw:border-lm-border tw:dark:border-dm-border tw:block tw:lg:table-cell tw:not-last:border-b-1 tw:lg:border-b-1 tw:before:lg:hidden tw:before:content-[attr(data-column)] tw:before:font-bold tw:before:mr-1"
class="border-lm-border dark:border-dm-border p-2 block lg:table-cell not-last:border-b-1 lg:border-b-1 before:lg:hidden before:content-[attr(data-column)] before:font-bold before:mr-1"
data-column="Auto-connect"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-check tw:text-brand"
class="svg-inline--fa fa-check text-lm-brand dark:text-dm-brand"
data-icon="check"
data-prefix="fas"
focusable="false"
id="autoConnectIcon"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
@@ -40,7 +39,7 @@ exports[`<ManageServersRow /> > renders auto-connect icon only if server is auto
</svg>
</td>
<td
class="tw:p-2 tw:border-lm-border tw:dark:border-dm-border tw:block tw:lg:table-cell tw:not-last:border-b-1 tw:lg:border-b-1 tw:before:lg:hidden tw:before:content-[attr(data-column)] tw:before:font-bold tw:before:mr-1 tw:font-bold"
class="border-lm-border dark:border-dm-border p-2 block lg:table-cell not-last:border-b-1 lg:border-b-1 before:lg:hidden before:content-[attr(data-column)] before:font-bold before:mr-1 font-bold"
data-column="Name"
>
<a
@@ -51,13 +50,13 @@ exports[`<ManageServersRow /> > renders auto-connect icon only if server is auto
</a>
</td>
<td
class="tw:p-2 tw:border-lm-border tw:dark:border-dm-border tw:block tw:lg:table-cell tw:not-last:border-b-1 tw:lg:border-b-1 tw:before:lg:hidden tw:before:content-[attr(data-column)] tw:before:font-bold tw:before:mr-1 tw:max-lg:border-b-0"
class="border-lm-border dark:border-dm-border p-2 block lg:table-cell not-last:border-b-1 lg:border-b-1 before:lg:hidden before:content-[attr(data-column)] before:font-bold before:mr-1 max-lg:border-b-0"
data-column="Base URL"
>
https://example.com
</td>
<td
class="tw:p-2 tw:border-lm-border tw:dark:border-dm-border tw:block tw:lg:table-cell tw:not-last:border-b-1 tw:lg:border-b-1 tw:before:lg:hidden tw:before:content-[attr(data-column)] tw:before:font-bold tw:before:mr-1 tw:text-right tw:max-lg:absolute tw:right-0 tw:-top-1 tw:mx-lg:pt-0"
class="border-lm-border dark:border-dm-border p-2 block lg:table-cell not-last:border-b-1 lg:border-b-1 before:lg:hidden before:content-[attr(data-column)] before:font-bold before:mr-1 text-right max-lg:absolute right-0 -top-1 mx-lg:pt-0"
>
<span>
ManageServersRowDropdown
@@ -72,27 +71,27 @@ exports[`<ManageServersRow /> > renders auto-connect icon only if server is auto
exports[`<ManageServersRow /> > renders auto-connect icon only if server is autoConnect 2`] = `
<div>
<table
class="tw:w-full"
class="w-full"
>
<thead
class="tw:hidden tw:lg:table-header-group"
class="hidden lg:table-header-group"
>
<tr
class="tw:group tw:lg:table-row tw:flex tw:flex-col tw:lg:border-0 tw:border-y-2 tw:border-lm-border tw:dark:border-dm-border"
class="group lg:table-row flex flex-col lg:border-0 border-y-2 border-lm-border dark:border-dm-border"
/>
</thead>
<tbody
class="tw:lg:table-row-group tw:flex tw:flex-col tw:gap-y-3"
class="lg:table-row-group flex flex-col gap-y-3"
>
<tr
class="tw:group tw:lg:table-row tw:flex tw:flex-col tw:lg:border-0 tw:border-y-2 tw:border-lm-border tw:dark:border-dm-border tw:hover:bg-lm-primary tw:dark:hover:bg-dm-primary tw:group-[&]/card:hover:bg-lm-secondary tw:dark:group-[&]/card:hover:bg-dm-secondary tw:relative"
class="group lg:table-row flex flex-col lg:border-0 border-y-2 border-lm-border dark:border-dm-border hover:bg-lm-primary dark:hover:bg-dm-primary group-[&]/card:hover:bg-lm-secondary dark:group-[&]/card:hover:bg-dm-secondary relative"
>
<td
class="tw:p-2 tw:border-lm-border tw:dark:border-dm-border tw:block tw:lg:table-cell tw:not-last:border-b-1 tw:lg:border-b-1 tw:before:lg:hidden tw:before:content-[attr(data-column)] tw:before:font-bold tw:before:mr-1"
class="border-lm-border dark:border-dm-border p-2 block lg:table-cell not-last:border-b-1 lg:border-b-1 before:lg:hidden before:content-[attr(data-column)] before:font-bold before:mr-1"
data-column="Auto-connect"
/>
<td
class="tw:p-2 tw:border-lm-border tw:dark:border-dm-border tw:block tw:lg:table-cell tw:not-last:border-b-1 tw:lg:border-b-1 tw:before:lg:hidden tw:before:content-[attr(data-column)] tw:before:font-bold tw:before:mr-1 tw:font-bold"
class="border-lm-border dark:border-dm-border p-2 block lg:table-cell not-last:border-b-1 lg:border-b-1 before:lg:hidden before:content-[attr(data-column)] before:font-bold before:mr-1 font-bold"
data-column="Name"
>
<a
@@ -103,13 +102,13 @@ exports[`<ManageServersRow /> > renders auto-connect icon only if server is auto
</a>
</td>
<td
class="tw:p-2 tw:border-lm-border tw:dark:border-dm-border tw:block tw:lg:table-cell tw:not-last:border-b-1 tw:lg:border-b-1 tw:before:lg:hidden tw:before:content-[attr(data-column)] tw:before:font-bold tw:before:mr-1 tw:max-lg:border-b-0"
class="border-lm-border dark:border-dm-border p-2 block lg:table-cell not-last:border-b-1 lg:border-b-1 before:lg:hidden before:content-[attr(data-column)] before:font-bold before:mr-1 max-lg:border-b-0"
data-column="Base URL"
>
https://example.com
</td>
<td
class="tw:p-2 tw:border-lm-border tw:dark:border-dm-border tw:block tw:lg:table-cell tw:not-last:border-b-1 tw:lg:border-b-1 tw:before:lg:hidden tw:before:content-[attr(data-column)] tw:before:font-bold tw:before:mr-1 tw:text-right tw:max-lg:absolute tw:right-0 tw:-top-1 tw:mx-lg:pt-0"
class="border-lm-border dark:border-dm-border p-2 block lg:table-cell not-last:border-b-1 lg:border-b-1 before:lg:hidden before:content-[attr(data-column)] before:font-bold before:mr-1 text-right max-lg:absolute right-0 -top-1 mx-lg:pt-0"
>
<span>
ManageServersRowDropdown

View File

@@ -3,18 +3,19 @@
exports[`<ManageServersRowDropdown /> > renders expected size and icon 1`] = `
<div>
<div
class="dropdown"
class="relative inline-block"
>
<button
aria-controls="«r9»"
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="dropdown-btn__toggle btn btn-primary btn-sm"
class="flex items-center rounded-md focus-ring cursor-pointer border border-lm-border dark:border-dm-border bg-lm-primary dark:bg-dm-primary group-[&]/card:bg-lm-input group-[&]/card:dark:bg-dm-input px-3 py-1.5 gap-x-2"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-ellipsis-vertical px-1"
class="svg-inline--fa fa-ellipsis-vertical "
data-icon="ellipsis-vertical"
data-prefix="fas"
focusable="false"
@@ -28,14 +29,6 @@ exports[`<ManageServersRowDropdown /> > renders expected size and icon 1`] = `
/>
</svg>
</button>
<div
aria-hidden="true"
class="w-100 dropdown-menu dropdown-menu-end"
data-bs-popper="static"
role="menu"
style="min-width: 210px;"
tabindex="-1"
/>
</div>
<span>
DeleteServerModal
@@ -47,18 +40,19 @@ exports[`<ManageServersRowDropdown /> > renders expected size and icon 1`] = `
exports[`<ManageServersRowDropdown /> > renders expected size and icon 2`] = `
<div>
<div
class="dropdown"
class="relative inline-block"
>
<button
aria-controls="«rb»"
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="dropdown-btn__toggle btn btn-primary btn-sm"
class="flex items-center rounded-md focus-ring cursor-pointer border border-lm-border dark:border-dm-border bg-lm-primary dark:bg-dm-primary group-[&]/card:bg-lm-input group-[&]/card:dark:bg-dm-input px-3 py-1.5 gap-x-2"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-ellipsis-vertical px-1"
class="svg-inline--fa fa-ellipsis-vertical "
data-icon="ellipsis-vertical"
data-prefix="fas"
focusable="false"
@@ -72,14 +66,6 @@ exports[`<ManageServersRowDropdown /> > renders expected size and icon 2`] = `
/>
</svg>
</button>
<div
aria-hidden="true"
class="w-100 dropdown-menu dropdown-menu-end"
data-bs-popper="static"
role="menu"
style="min-width: 170px;"
tabindex="-1"
/>
</div>
<span>
DeleteServerModal

View File

@@ -21,9 +21,11 @@ export default defineConfig({
manifestFilename: 'manifest.json',
manifest,
})],
build: {
outDir: 'build',
},
server: {
port: 3000,
watch: {
@@ -31,6 +33,7 @@ export default defineConfig({
ignored: ['**/.idea/**', '**/.git/**', '**/build/**', '**/coverage/**', '**/test/**'],
},
},
base: !homepage ? undefined : homepage, // Not using just homepage because empty string should be discarded
// Vitest config