Compare commits
136 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eab3b6935 | ||
|
|
ea420936fa | ||
|
|
979f002f0c | ||
|
|
54fe38a92b | ||
|
|
1e2618d3b8 | ||
|
|
6b0fc80f1e | ||
|
|
d6d0539fa7 | ||
|
|
81742cbbaf | ||
|
|
300cd50310 | ||
|
|
d227a5db78 | ||
|
|
4f2dd9d240 | ||
|
|
9f750f7f6b | ||
|
|
e5a1563f39 | ||
|
|
bc3634e181 | ||
|
|
3343fba861 | ||
|
|
eff4712f69 | ||
|
|
2788264265 | ||
|
|
6fe52143a8 | ||
|
|
058b38528f | ||
|
|
d1619b3723 | ||
|
|
0bdf5f206e | ||
|
|
34aca8ff3f | ||
|
|
b4db7fdf11 | ||
|
|
d10d7fd96d | ||
|
|
9c0c2fc3f9 | ||
|
|
913b3c5fc1 | ||
|
|
6dd3fd275a | ||
|
|
2b05016586 | ||
|
|
52cc943185 | ||
|
|
edd6f7e807 | ||
|
|
d13e6fb0cc | ||
|
|
2208e9b562 | ||
|
|
dadcdb22f5 | ||
|
|
4246b85b43 | ||
|
|
4d259e50f3 | ||
|
|
e5fff0bc08 | ||
|
|
7a3aab2482 | ||
|
|
455ac9a275 | ||
|
|
b70cebb3f2 | ||
|
|
e3b24eea78 | ||
|
|
a96a48ed19 | ||
|
|
6114763e39 | ||
|
|
575f86cc34 | ||
|
|
93cf8b1258 | ||
|
|
76b7523d4a | ||
|
|
c2c1037c01 | ||
|
|
d3feee301e | ||
|
|
531a61a57b | ||
|
|
af252846f7 | ||
|
|
a94fa74a12 | ||
|
|
3e24c3b61b | ||
|
|
581e809fd2 | ||
|
|
e2ceb52c75 | ||
|
|
f975a71230 | ||
|
|
03df998b86 | ||
|
|
b758781407 | ||
|
|
8dec3a5277 | ||
|
|
191d6af4bd | ||
|
|
543b0a5a80 | ||
|
|
b86a644dcd | ||
|
|
e7a3ec3ee2 | ||
|
|
1f23be963a | ||
|
|
726050e777 | ||
|
|
06af621e7c | ||
|
|
30e67151fe | ||
|
|
0ba34cc8eb | ||
|
|
7a5ae2f19e | ||
|
|
de27e453c3 | ||
|
|
b913a14105 | ||
|
|
2bf0bc8334 | ||
|
|
b5529d3a8c | ||
|
|
b5a091861d | ||
|
|
931f980e16 | ||
|
|
5f091eada1 | ||
|
|
bd1a16a8f8 | ||
|
|
6f8669cec7 | ||
|
|
7d52ae3e7d | ||
|
|
3e194246ab | ||
|
|
cce28116a8 | ||
|
|
90125d51f2 | ||
|
|
dc670b8693 | ||
|
|
3f8a33e77c | ||
|
|
3763a1541a | ||
|
|
4bd61d1f80 | ||
|
|
713c6fc7d9 | ||
|
|
b5efcda25e | ||
|
|
12d7e76731 | ||
|
|
249c948d11 | ||
|
|
b8d0b1bd2a | ||
|
|
77b5c93652 | ||
|
|
137be6962c | ||
|
|
357406d2a4 | ||
|
|
4ba98ede77 | ||
|
|
89f250bfb7 | ||
|
|
425c70bfd8 | ||
|
|
74ffd63828 | ||
|
|
f8a2f81ce4 | ||
|
|
e8f05783a9 | ||
|
|
ae0f2282de | ||
|
|
cc0091f753 | ||
|
|
1e84db4a47 | ||
|
|
8f59da1ce7 | ||
|
|
e46751844c | ||
|
|
d131ac5d03 | ||
|
|
878c82e8f0 | ||
|
|
69fed72a81 | ||
|
|
bda6388100 | ||
|
|
1d6a9ec563 | ||
|
|
56517b9840 | ||
|
|
03a0945a29 | ||
|
|
023a0ca824 | ||
|
|
f6a0910c40 | ||
|
|
869708469d | ||
|
|
f51c8e7643 | ||
|
|
256d886930 | ||
|
|
e74a4b5320 | ||
|
|
751ffdab61 | ||
|
|
fec1fc3c76 | ||
|
|
35d27ffb04 | ||
|
|
3ba6b58b9e | ||
|
|
8ce81771b2 | ||
|
|
17c1ff1c4c | ||
|
|
9a2f83b2b5 | ||
|
|
dfd9a3efb8 | ||
|
|
5e630368e6 | ||
|
|
5327160774 | ||
|
|
b092c86658 | ||
|
|
2988b24fac | ||
|
|
b49b0ebf16 | ||
|
|
45f972e351 | ||
|
|
a824349ad2 | ||
|
|
2fd933fc32 | ||
|
|
b48cf19acf | ||
|
|
669a11186b | ||
|
|
355cdd2550 | ||
|
|
34a08e461f |
24
.github/DISCUSSION_TEMPLATE/q-a.yml
vendored
@@ -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. -->'
|
||||
7
.github/ISSUE_TEMPLATE.md
vendored
@@ -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.
|
||||
-->
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
||||
|
||||
4
.github/dependabot.yml
vendored
@@ -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:
|
||||
|
||||
20
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
41
package.json
@@ -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"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 642 B After Width: | Height: | Size: 662 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 287 B After Width: | Height: | Size: 319 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 381 B After Width: | Height: | Size: 420 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 437 B After Width: | Height: | Size: 509 B |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 466 B After Width: | Height: | Size: 570 B |
|
Before Width: | Height: | Size: 551 B After Width: | Height: | Size: 642 B |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 638 B After Width: | Height: | Size: 799 B |
|
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 834 B |
|
Before Width: | Height: | Size: 750 B After Width: | Height: | Size: 908 B |
|
Before Width: | Height: | Size: 783 B After Width: | Height: | Size: 973 B |
|
Before Width: | Height: | Size: 984 B After Width: | Height: | Size: 1.1 KiB |
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} /></>}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} /> 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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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} /> - </>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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} />)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||