mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-04-11 00:56:20 +00:00
Merge pull request #1491 from acelaya-forks/feature/initial-tailwind-components
Initial tailwind components
This commit is contained in:
@@ -22,3 +22,12 @@ afterEach(() => {
|
|||||||
HTMLCanvasElement.prototype.getContext = (() => {}) as any;
|
HTMLCanvasElement.prototype.getContext = (() => {}) as any;
|
||||||
(global as any).scrollTo = () => {};
|
(global as any).scrollTo = () => {};
|
||||||
(global as any).matchMedia = () => ({ matches: false });
|
(global as any).matchMedia = () => ({ matches: false });
|
||||||
|
|
||||||
|
HTMLDialogElement.prototype.showModal = function() {
|
||||||
|
this.setAttribute('open', '');
|
||||||
|
};
|
||||||
|
HTMLDialogElement.prototype.close = function() {
|
||||||
|
this.removeAttribute('open');
|
||||||
|
this.dispatchEvent(new CloseEvent('close'));
|
||||||
|
this.dispatchEvent(new CloseEvent('cancel'));
|
||||||
|
};
|
||||||
|
|||||||
789
package-lock.json
generated
789
package-lock.json
generated
@@ -41,6 +41,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@shlinkio/eslint-config-js-coding-standard": "~3.5.0",
|
"@shlinkio/eslint-config-js-coding-standard": "~3.5.0",
|
||||||
"@stylistic/eslint-plugin": "^4.2.0",
|
"@stylistic/eslint-plugin": "^4.2.0",
|
||||||
|
"@tailwindcss/vite": "^4.0.17",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"sass": "^1.86.3",
|
"sass": "^1.86.3",
|
||||||
|
"tailwindcss": "^4.0.17",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.29.0",
|
"typescript-eslint": "^8.29.0",
|
||||||
"vite": "^6.2.5",
|
"vite": "^6.2.5",
|
||||||
@@ -3532,6 +3534,244 @@
|
|||||||
"sourcemap-codec": "^1.4.8"
|
"sourcemap-codec": "^1.4.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/node": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-LIdNwcqyY7578VpofXyqjH6f+3fP4nrz7FBLki5HpzqjYfXdF2m/eW18ZfoKePtDGg90Bvvfpov9d2gy5XVCbg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"enhanced-resolve": "^5.18.1",
|
||||||
|
"jiti": "^2.4.2",
|
||||||
|
"tailwindcss": "4.0.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-B4OaUIRD2uVrULpAD1Yksx2+wNarQr2rQh65nXqaqbLY1jCd8fO+3KLh/+TH4Hzh2NTHQvgxVbPdUDOtLk7vAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tailwindcss/oxide-android-arm64": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-darwin-x64": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": "4.0.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-3RfO0ZK64WAhop+EbHeyxGThyDr/fYhxPzDbEQjD2+v7ZhKTb2svTWy+KK+J1PHATus2/CQGAGp7pHY/8M8ugg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-e1uayxFQCCDuzTk9s8q7MC5jFN42IY7nzcr5n0Mw/AcUHwD6JaBkXnATkD924ZsHyPDvddnusIEvkgLd2CiREg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-d6z7HSdOKfXQ0HPlVx1jduUf/YtBuCCtEDIEFeBCzgRRtDsUuRtofPqxIVaSCUTOk5+OfRLonje6n9dF6AH8wQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-EjrVa6lx3wzXz3l5MsdOGtYIsRjgs5Mru6lDv4RuiXpguWeOb3UzGJ7vw7PEzcFadKNvNslEQqoAABeMezprxQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-65zXfCOdi8wuaY0Ye6qMR5LAXokHYtrGvo9t/NmxvSZtCCitXV/gzJ/WP5ksXPhff1SV5rov0S+ZIZU+/4eyCQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-+aaq6hJ8ioTdbJV5IA1WjWgLmun4T7eYLTvJIToiXLHy5JzUERRbIZjAcjgK9qXMwnvuu7rqpxzej+hGoEcG5g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-/FhWgZCdUGAeYHYnZKekiOC0aXFiBIoNCA0bwzkICiMYS5Rtx2KxFfMUXQVnl4uZRblG5ypt5vpPhVaXgGk80w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-gELJzOHK6GDoIpm/539Golvk+QWZjxQcbkKq9eB2kzNkOvrP0xc5UPgO9bIMNt1M48mO8ZeNenCMGt6tfkvVBg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-68NwxcJrZn94IOW4TysMIbYv5AlM6So1luTlbYUDIGnKma1yTFGBRNEJ+SacJ3PZE2rgcTBNRHX1TB4EQ/XEHw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-AkBO8efP2/7wkEXkNlXzRD4f/7WerqKHlc6PWb5v0jGbbm22DFBLbIM19IJQ3b+tNewQZa+WnPOaGm0SmwMNjw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-7/DTEvXcoWlqX0dAlcN0zlmcEu9xSermuo7VNGX9tJ3nYMdo735SHvbrHDln1+LYfF6NhJ3hjbpbjkMOAGmkDg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/vite": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-HJbBYDlDVg5cvYZzECb6xwc1IDCEM3uJi3hEZp3BjZGCNGJcTsnCpan+z+VMW0zo6gR0U6O6ElqU1OoZ74Dhww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/node": "4.0.17",
|
||||||
|
"@tailwindcss/oxide": "4.0.17",
|
||||||
|
"lightningcss": "1.29.2",
|
||||||
|
"tailwindcss": "4.0.17"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "^5.2.0 || ^6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@testing-library/dom": {
|
"node_modules/@testing-library/dom": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz",
|
||||||
@@ -4762,9 +5002,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001680",
|
"version": "1.0.30001709",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001709.tgz",
|
||||||
"integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==",
|
"integrity": "sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -4779,7 +5019,8 @@
|
|||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
"node_modules/chai": {
|
"node_modules/chai": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
@@ -5400,6 +5641,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/enhanced-resolve": {
|
||||||
|
"version": "5.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||||
|
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.4",
|
||||||
|
"tapable": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
@@ -7339,6 +7594,16 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jiti": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -7533,6 +7798,255 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lightningcss": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"lightningcss-darwin-arm64": "1.29.2",
|
||||||
|
"lightningcss-darwin-x64": "1.29.2",
|
||||||
|
"lightningcss-freebsd-x64": "1.29.2",
|
||||||
|
"lightningcss-linux-arm-gnueabihf": "1.29.2",
|
||||||
|
"lightningcss-linux-arm64-gnu": "1.29.2",
|
||||||
|
"lightningcss-linux-arm64-musl": "1.29.2",
|
||||||
|
"lightningcss-linux-x64-gnu": "1.29.2",
|
||||||
|
"lightningcss-linux-x64-musl": "1.29.2",
|
||||||
|
"lightningcss-win32-arm64-msvc": "1.29.2",
|
||||||
|
"lightningcss-win32-x64-msvc": "1.29.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss/node_modules/detect-libc": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -9308,6 +9822,23 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwindcss": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-OErSiGzRa6rLiOvaipsDZvLMSpsBZ4ysB4f0VKGXUrjw2jfkJRd6kjRKV2+ZmTCNvwtvgdDam5D7w6WXsdLJZw==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tapable": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/temp-dir": {
|
"node_modules/temp-dir": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
||||||
@@ -12847,6 +13378,125 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@tailwindcss/node": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-LIdNwcqyY7578VpofXyqjH6f+3fP4nrz7FBLki5HpzqjYfXdF2m/eW18ZfoKePtDGg90Bvvfpov9d2gy5XVCbg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"enhanced-resolve": "^5.18.1",
|
||||||
|
"jiti": "^2.4.2",
|
||||||
|
"tailwindcss": "4.0.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tailwindcss/oxide": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-B4OaUIRD2uVrULpAD1Yksx2+wNarQr2rQh65nXqaqbLY1jCd8fO+3KLh/+TH4Hzh2NTHQvgxVbPdUDOtLk7vAw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@tailwindcss/oxide-android-arm64": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-darwin-x64": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": "4.0.17",
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": "4.0.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@tailwindcss/oxide-android-arm64": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-3RfO0ZK64WAhop+EbHeyxGThyDr/fYhxPzDbEQjD2+v7ZhKTb2svTWy+KK+J1PHATus2/CQGAGp7pHY/8M8ugg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@tailwindcss/oxide-darwin-arm64": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-e1uayxFQCCDuzTk9s8q7MC5jFN42IY7nzcr5n0Mw/AcUHwD6JaBkXnATkD924ZsHyPDvddnusIEvkgLd2CiREg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@tailwindcss/oxide-darwin-x64": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-d6z7HSdOKfXQ0HPlVx1jduUf/YtBuCCtEDIEFeBCzgRRtDsUuRtofPqxIVaSCUTOk5+OfRLonje6n9dF6AH8wQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@tailwindcss/oxide-freebsd-x64": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-EjrVa6lx3wzXz3l5MsdOGtYIsRjgs5Mru6lDv4RuiXpguWeOb3UzGJ7vw7PEzcFadKNvNslEQqoAABeMezprxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-65zXfCOdi8wuaY0Ye6qMR5LAXokHYtrGvo9t/NmxvSZtCCitXV/gzJ/WP5ksXPhff1SV5rov0S+ZIZU+/4eyCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@tailwindcss/oxide-linux-arm64-gnu": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-+aaq6hJ8ioTdbJV5IA1WjWgLmun4T7eYLTvJIToiXLHy5JzUERRbIZjAcjgK9qXMwnvuu7rqpxzej+hGoEcG5g==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@tailwindcss/oxide-linux-arm64-musl": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-/FhWgZCdUGAeYHYnZKekiOC0aXFiBIoNCA0bwzkICiMYS5Rtx2KxFfMUXQVnl4uZRblG5ypt5vpPhVaXgGk80w==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-gELJzOHK6GDoIpm/539Golvk+QWZjxQcbkKq9eB2kzNkOvrP0xc5UPgO9bIMNt1M48mO8ZeNenCMGt6tfkvVBg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@tailwindcss/oxide-linux-x64-musl": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-68NwxcJrZn94IOW4TysMIbYv5AlM6So1luTlbYUDIGnKma1yTFGBRNEJ+SacJ3PZE2rgcTBNRHX1TB4EQ/XEHw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-AkBO8efP2/7wkEXkNlXzRD4f/7WerqKHlc6PWb5v0jGbbm22DFBLbIM19IJQ3b+tNewQZa+WnPOaGm0SmwMNjw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-7/DTEvXcoWlqX0dAlcN0zlmcEu9xSermuo7VNGX9tJ3nYMdo735SHvbrHDln1+LYfF6NhJ3hjbpbjkMOAGmkDg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@tailwindcss/vite": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-HJbBYDlDVg5cvYZzECb6xwc1IDCEM3uJi3hEZp3BjZGCNGJcTsnCpan+z+VMW0zo6gR0U6O6ElqU1OoZ74Dhww==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@tailwindcss/node": "4.0.17",
|
||||||
|
"@tailwindcss/oxide": "4.0.17",
|
||||||
|
"lightningcss": "1.29.2",
|
||||||
|
"tailwindcss": "4.0.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@testing-library/dom": {
|
"@testing-library/dom": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz",
|
||||||
@@ -13696,9 +14346,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
"caniuse-lite": {
|
||||||
"version": "1.0.30001680",
|
"version": "1.0.30001709",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001709.tgz",
|
||||||
"integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==",
|
"integrity": "sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"chai": {
|
"chai": {
|
||||||
@@ -14130,6 +14780,16 @@
|
|||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"enhanced-resolve": {
|
||||||
|
"version": "5.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||||
|
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"graceful-fs": "^4.2.4",
|
||||||
|
"tapable": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"entities": {
|
"entities": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
@@ -15507,6 +16167,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jiti": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"js-tokens": {
|
"js-tokens": {
|
||||||
"version": "4.0.0"
|
"version": "4.0.0"
|
||||||
},
|
},
|
||||||
@@ -15653,6 +16319,103 @@
|
|||||||
"type-check": "~0.4.0"
|
"type-check": "~0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"lightningcss": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"detect-libc": "^2.0.3",
|
||||||
|
"lightningcss-darwin-arm64": "1.29.2",
|
||||||
|
"lightningcss-darwin-x64": "1.29.2",
|
||||||
|
"lightningcss-freebsd-x64": "1.29.2",
|
||||||
|
"lightningcss-linux-arm-gnueabihf": "1.29.2",
|
||||||
|
"lightningcss-linux-arm64-gnu": "1.29.2",
|
||||||
|
"lightningcss-linux-arm64-musl": "1.29.2",
|
||||||
|
"lightningcss-linux-x64-gnu": "1.29.2",
|
||||||
|
"lightningcss-linux-x64-musl": "1.29.2",
|
||||||
|
"lightningcss-win32-arm64-msvc": "1.29.2",
|
||||||
|
"lightningcss-win32-x64-msvc": "1.29.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lightningcss-darwin-arm64": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss-darwin-x64": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss-freebsd-x64": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss-linux-arm64-gnu": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss-linux-arm64-musl": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss-linux-x64-gnu": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss-linux-x64-musl": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss-win32-arm64-msvc": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss-win32-x64-msvc": {
|
||||||
|
"version": "1.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz",
|
||||||
|
"integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"locate-path": {
|
"locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -16861,6 +17624,18 @@
|
|||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"tailwindcss": {
|
||||||
|
"version": "4.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.17.tgz",
|
||||||
|
"integrity": "sha512-OErSiGzRa6rLiOvaipsDZvLMSpsBZ4ysB4f0VKGXUrjw2jfkJRd6kjRKV2+ZmTCNvwtvgdDam5D7w6WXsdLJZw==",
|
||||||
|
"devOptional": true
|
||||||
|
},
|
||||||
|
"tapable": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"temp-dir": {
|
"temp-dir": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@shlinkio/eslint-config-js-coding-standard": "~3.5.0",
|
"@shlinkio/eslint-config-js-coding-standard": "~3.5.0",
|
||||||
"@stylistic/eslint-plugin": "^4.2.0",
|
"@stylistic/eslint-plugin": "^4.2.0",
|
||||||
|
"@tailwindcss/vite": "^4.0.17",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
@@ -75,6 +76,7 @@
|
|||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"sass": "^1.86.3",
|
"sass": "^1.86.3",
|
||||||
|
"tailwindcss": "^4.0.17",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.29.0",
|
"typescript-eslint": "^8.29.0",
|
||||||
"vite": "^6.2.5",
|
"vite": "^6.2.5",
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
@use '../../node_modules/@shlinkio/shlink-frontend-kit/dist/base';
|
|
||||||
|
|
||||||
.app-container {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app {
|
|
||||||
padding-top: base.$headerHeight;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shlink-wrapper {
|
|
||||||
min-height: 100%;
|
|
||||||
padding-bottom: base.$footer-height + base.$footer-margin;
|
|
||||||
margin-bottom: -(base.$footer-height + base.$footer-margin);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shlink-footer {
|
|
||||||
height: base.$footer-height;
|
|
||||||
margin-top: base.$footer-margin;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
@media (min-width: base.$mdMin) {
|
|
||||||
padding: 0 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ import type { FCWithDeps } from '../container/utils';
|
|||||||
import { componentFactory, useDependencies } from '../container/utils';
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
import type { ServersMap } from '../servers/data';
|
import type { ServersMap } from '../servers/data';
|
||||||
import { forceUpdate } from '../utils/helpers/sw';
|
import { forceUpdate } from '../utils/helpers/sw';
|
||||||
import './App.scss';
|
|
||||||
|
|
||||||
type AppProps = {
|
type AppProps = {
|
||||||
fetchServers: () => void;
|
fetchServers: () => void;
|
||||||
@@ -62,11 +61,17 @@ const App: FCWithDeps<AppProps, AppDeps> = (
|
|||||||
}, [settings.ui?.theme]);
|
}, [settings.ui?.theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-fluid app-container">
|
<div className="tw:px-3 tw:h-full">
|
||||||
<MainHeader />
|
<MainHeader />
|
||||||
|
|
||||||
<div className="app">
|
<div className="tw:h-full tw:pt-(--header-height)">
|
||||||
<div className={clsx('shlink-wrapper', { 'd-flex align-items-center pt-3': isHome })}>
|
<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 },
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={<Home />} />
|
<Route index element={<Home />} />
|
||||||
<Route path="/settings">
|
<Route path="/settings">
|
||||||
@@ -82,12 +87,12 @@ const App: FCWithDeps<AppProps, AppDeps> = (
|
|||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shlink-footer">
|
<div className="tw:h-(--footer-height) tw:mt-(--footer-margin) tw:md:px-4">
|
||||||
<ShlinkVersionsContainer />
|
<ShlinkVersionsContainer />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AppUpdateBanner isOpen={appUpdated} toggle={resetAppUpdate} forceUpdate={forceUpdate} />
|
<AppUpdateBanner isOpen={appUpdated} onClose={resetAppUpdate} forceUpdate={forceUpdate} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
@use '../../node_modules/@shlinkio/shlink-frontend-kit/dist/base';
|
|
||||||
@use '../utils/mixins/horizontal-align';
|
|
||||||
|
|
||||||
.app-update-banner.app-update-banner {
|
|
||||||
@include horizontal-align.horizontal-align();
|
|
||||||
|
|
||||||
position: fixed;
|
|
||||||
top: base.$headerHeight - 25px;
|
|
||||||
padding: 0 4rem 0 0;
|
|
||||||
z-index: 1040;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--text-color);
|
|
||||||
text-align: center;
|
|
||||||
width: 700px;
|
|
||||||
max-width: calc(100% - 30px);
|
|
||||||
box-shadow: 0 0 1rem var(--brand-color);
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,47 @@
|
|||||||
import { faSyncAlt as reloadIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faSyncAlt as reloadIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { SimpleCard, useToggle } from '@shlinkio/shlink-frontend-kit';
|
import { useToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
import type { MouseEventHandler } from 'react';
|
import { Button, Card, CloseButton } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import { forwardRef, useCallback } from 'react';
|
import { clsx } from 'clsx';
|
||||||
import { Alert, Button } from 'reactstrap';
|
import type { FC } from 'react';
|
||||||
import './AppUpdateBanner.scss';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
interface AppUpdateBannerProps {
|
interface AppUpdateBannerProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
toggle: MouseEventHandler<any>;
|
onClose: () => void;
|
||||||
forceUpdate: () => void;
|
forceUpdate: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppUpdateBanner = forwardRef<HTMLElement, AppUpdateBannerProps>(({ isOpen, toggle, forceUpdate }, ref) => {
|
export const AppUpdateBanner: FC<AppUpdateBannerProps> = ({ isOpen, onClose, forceUpdate }) => {
|
||||||
const [isUpdating,, setUpdating] = useToggle();
|
const [isUpdating,, setUpdating] = useToggle();
|
||||||
const update = useCallback(() => {
|
const update = useCallback(() => {
|
||||||
setUpdating();
|
setUpdating();
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}, [forceUpdate, setUpdating]);
|
}, [forceUpdate, setUpdating]);
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert className="app-update-banner" isOpen={isOpen} toggle={toggle} tag={SimpleCard} color="secondary" innerRef={ref}>
|
<Card
|
||||||
<h4 className="mb-4">This app has just been updated!</h4>
|
role="alert"
|
||||||
<p className="mb-0">
|
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]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Card.Header className="tw:flex tw:items-center tw: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">
|
||||||
Restart it to enjoy the new features.
|
Restart it to enjoy the new features.
|
||||||
<Button role="button" disabled={isUpdating} className="ms-2" color="secondary" size="sm" onClick={update}>
|
<Button disabled={isUpdating} variant="secondary" solid onClick={update}>
|
||||||
{!isUpdating && <>Restart now <FontAwesomeIcon icon={reloadIcon} className="ms-1" /></>}
|
{!isUpdating && <>Restart now <FontAwesomeIcon icon={reloadIcon} /></>}
|
||||||
{isUpdating && <>Restarting...</>}
|
{isUpdating && <>Restarting...</>}
|
||||||
</Button>
|
</Button>
|
||||||
</p>
|
</Card.Body>
|
||||||
</Alert>
|
</Card>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
import { Button } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import type { PropsWithChildren, ReactNode } from 'react';
|
import type { PropsWithChildren, ReactNode } from 'react';
|
||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Button } from 'reactstrap';
|
import { ErrorLayout } from './ErrorLayout';
|
||||||
|
|
||||||
type ErrorHandlerProps = PropsWithChildren<{
|
type ErrorHandlerProps = PropsWithChildren<{
|
||||||
location?: typeof window.location;
|
location?: typeof window.location;
|
||||||
@@ -33,14 +33,11 @@ export class ErrorHandler extends Component<ErrorHandlerProps, ErrorHandlerState
|
|||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="home">
|
<ErrorLayout title="Oops! This is awkward :S">
|
||||||
<SimpleCard className="p-4">
|
|
||||||
<h1>Oops! This is awkward :S</h1>
|
|
||||||
<p>It seems that something went wrong. Try refreshing the page or just click this button.</p>
|
<p>It seems that something went wrong. Try refreshing the page or just click this button.</p>
|
||||||
<br />
|
<br />
|
||||||
<Button outline color="primary" onClick={() => location.reload()}>Take me back</Button>
|
<Button size="lg" onClick={() => location.reload()}>Take me back</Button>
|
||||||
</SimpleCard>
|
</ErrorLayout>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
src/common/ErrorLayout.tsx
Normal file
15
src/common/ErrorLayout.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { SimpleCard } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
|
import type { FC, PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
export type ErrorLayoutProps = PropsWithChildren<{
|
||||||
|
title: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
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">
|
||||||
|
<h2>{title}</h2>
|
||||||
|
{children}
|
||||||
|
</SimpleCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
@use '../../node_modules/@shlinkio/shlink-frontend-kit/dist/base';
|
|
||||||
|
|
||||||
.home__title {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
|
|
||||||
@media (min-width: base.$mdMin) {
|
|
||||||
font-size: 2.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home__servers-container {
|
|
||||||
@media (min-width: base.$mdMin) {
|
|
||||||
border-left: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
|
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { Button, Card } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
import { Link, useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { Card } from 'reactstrap';
|
|
||||||
import type { ServersMap } from '../servers/data';
|
import type { ServersMap } from '../servers/data';
|
||||||
import { ServersListGroup } from '../servers/ServersListGroup';
|
import { ServersListGroup } from '../servers/ServersListGroup';
|
||||||
import { ShlinkLogo } from './img/ShlinkLogo';
|
import { ShlinkLogo } from './img/ShlinkLogo';
|
||||||
import './Home.scss';
|
|
||||||
|
|
||||||
interface HomeProps {
|
export type HomeProps = {
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Home = ({ servers }: HomeProps) => {
|
export const Home = ({ servers }: HomeProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -28,42 +27,42 @@ export const Home = ({ servers }: HomeProps) => {
|
|||||||
}, [serversList, navigate]);
|
}, [serversList, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-100">
|
<div className="tw:w-full">
|
||||||
<Card className="mx-auto" style={{ maxWidth: '720px' }}>
|
<Card className="tw:mx-auto tw:max-w-[720px] tw:overflow-hidden">
|
||||||
<div className="d-flex flex-column flex-md-row">
|
<div className="tw:flex tw:flex-col tw:md:flex-row">
|
||||||
<div className="p-4 d-none d-md-flex align-items-center" style={{ width: '40%' }}>
|
<div className="tw:p-6 tw:hidden tw:md:flex tw:items-center tw:w-[40%]">
|
||||||
<div className="w-100">
|
<div className="tw:w-full">
|
||||||
<ShlinkLogo />
|
<ShlinkLogo />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="home__servers-container flex-grow-1">
|
<div className="tw:md:border-l tw:border-lm-border tw:dark:border-dm-border tw:flex-grow">
|
||||||
<h1
|
<h1
|
||||||
className={clsx('home__title p-4 text-center m-0', { 'border-bottom': !hasServers })}
|
className={clsx(
|
||||||
style={{ borderColor: 'var(--border-color) !important' }}
|
'tw:p-4 tw:text-center tw:border-lm-border tw:dark:border-dm-border',
|
||||||
|
{ 'tw:border-b': !hasServers },
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Welcome!
|
Welcome!
|
||||||
</h1>
|
</h1>
|
||||||
<ServersListGroup embedded servers={serversList}>
|
{hasServers ? <ServersListGroup servers={serversList} /> : (
|
||||||
{!hasServers && (
|
<div className="tw:p-6 tw:text-center tw:flex tw:flex-col tw:gap-12 tw:text-xl">
|
||||||
<div className="p-4 text-center d-flex flex-column gap-5">
|
<p>This application will help you manage your Shlink servers.</p>
|
||||||
<p className="mb-0">This application will help you manage your Shlink servers.</p>
|
<p>
|
||||||
<p className="mb-0">
|
<Button to="/server/create" size="lg" inline>
|
||||||
<Link to="/server/create" className="btn btn-outline-primary btn-lg me-2">
|
<FontAwesomeIcon icon={faPlus} /> Add a server
|
||||||
<FontAwesomeIcon icon={faPlus} /> <span className="ms-1">Add a server</span>
|
</Button>
|
||||||
</Link>
|
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-0">
|
<p>
|
||||||
<ExternalLink href="https://shlink.io/documentation">
|
<ExternalLink href="https://shlink.io/documentation">
|
||||||
<small>
|
<small>
|
||||||
<span className="me-2">Learn more about Shlink</span>
|
<span className="tw:mr-2">Learn more about Shlink</span>
|
||||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||||
</small>
|
</small>
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ServersListGroup>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
.main-header.main-header {
|
|
||||||
color: white;
|
|
||||||
background-color: var(--brand-color) !important;
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
color: inherit !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-header__brand-logo {
|
|
||||||
width: 26px;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-header__toggle-icon {
|
|
||||||
width: 20px;
|
|
||||||
transition: transform 300ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-header__toggle-icon--opened {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,6 @@ import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } f
|
|||||||
import type { FCWithDeps } from '../container/utils';
|
import type { FCWithDeps } from '../container/utils';
|
||||||
import { componentFactory, useDependencies } from '../container/utils';
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
import { ShlinkLogo } from './img/ShlinkLogo';
|
import { ShlinkLogo } from './img/ShlinkLogo';
|
||||||
import './MainHeader.scss';
|
|
||||||
|
|
||||||
type MainHeaderDeps = {
|
type MainHeaderDeps = {
|
||||||
ServersDropdown: FC;
|
ServersDropdown: FC;
|
||||||
@@ -25,20 +24,22 @@ const MainHeader: FCWithDeps<unknown, MainHeaderDeps> = () => {
|
|||||||
useEffect(collapse, [location, collapse]);
|
useEffect(collapse, [location, collapse]);
|
||||||
|
|
||||||
const settingsPath = '/settings';
|
const settingsPath = '/settings';
|
||||||
const toggleClass = clsx('main-header__toggle-icon', { 'main-header__toggle-icon--opened': isNotCollapsed });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar color="primary" dark fixed="top" className="main-header" expand="md">
|
<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="/">
|
<NavbarBrand tag={Link} to="/">
|
||||||
<ShlinkLogo className="main-header__brand-logo" color="white" /> Shlink
|
<ShlinkLogo className="tw:inline tw:w-7 tw:mr-1" color="white" /> Shlink
|
||||||
</NavbarBrand>
|
</NavbarBrand>
|
||||||
|
|
||||||
<NavbarToggler onClick={toggleCollapse}>
|
<NavbarToggler onClick={toggleCollapse}>
|
||||||
<FontAwesomeIcon icon={arrowIcon} className={toggleClass} />
|
<FontAwesomeIcon
|
||||||
|
icon={arrowIcon}
|
||||||
|
className={clsx('tw:transition-transform tw:duration-300', { 'tw:rotate-180': isNotCollapsed })}
|
||||||
|
/>
|
||||||
</NavbarToggler>
|
</NavbarToggler>
|
||||||
|
|
||||||
<Collapse navbar isOpen={isNotCollapsed}>
|
<Collapse navbar isOpen={isNotCollapsed}>
|
||||||
<Nav navbar className="ms-auto">
|
<Nav navbar className="tw:ml-auto">
|
||||||
<NavItem>
|
<NavItem>
|
||||||
<NavLink tag={Link} to={settingsPath} active={pathname.startsWith(settingsPath)}>
|
<NavLink tag={Link} to={settingsPath} active={pathname.startsWith(settingsPath)}>
|
||||||
<FontAwesomeIcon icon={cogsIcon} /> Settings
|
<FontAwesomeIcon icon={cogsIcon} /> Settings
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
@use '../../node_modules/@shlinkio/shlink-frontend-kit/dist/base';
|
|
||||||
|
|
||||||
.no-menu-wrapper {
|
|
||||||
padding: 15px 0 0;
|
|
||||||
|
|
||||||
@media (min-width: base.$mdMin) {
|
|
||||||
padding: 30px 20px 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import type { FC, PropsWithChildren } from 'react';
|
import type { FC, PropsWithChildren } from 'react';
|
||||||
import './NoMenuLayout.scss';
|
|
||||||
|
|
||||||
export type NoMenuLayoutProps = PropsWithChildren & {
|
export type NoMenuLayoutProps = PropsWithChildren & {
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoMenuLayout: FC<NoMenuLayoutProps> = ({ children, className }) => (
|
export const NoMenuLayout: FC<NoMenuLayoutProps> = ({ children, className }) => (
|
||||||
<div className={clsx('no-menu-wrapper container-xl', className)}>{children}</div>
|
<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)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
import { Button } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import type { FC, PropsWithChildren } from 'react';
|
import type { FC, PropsWithChildren } from 'react';
|
||||||
import { Link } from 'react-router';
|
import { ErrorLayout } from './ErrorLayout';
|
||||||
|
|
||||||
type NotFoundProps = PropsWithChildren<{ to?: string }>;
|
type NotFoundProps = PropsWithChildren<{ to?: string }>;
|
||||||
|
|
||||||
export const NotFound: FC<NotFoundProps> = ({ to = '/', children = 'Home' }) => (
|
export const NotFound: FC<NotFoundProps> = ({ to = '/', children = 'Home' }) => (
|
||||||
<div className="home">
|
<ErrorLayout title="Oops! We could not find requested route.">
|
||||||
<SimpleCard className="p-4">
|
|
||||||
<h2>Oops! We could not find requested route.</h2>
|
|
||||||
<p>
|
<p>
|
||||||
Use your browser's back button to navigate to the page you have previously come from, or just press this
|
Use your browser's back button to navigate to the page you have previously come from, or just press this
|
||||||
button.
|
button.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
<Link to={to} className="btn btn-outline-primary btn-lg">{children}</Link>
|
<Button inline to={to} size="lg">{children}</Button>
|
||||||
</SimpleCard>
|
</ErrorLayout>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export interface ShlinkVersionsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VersionLink = ({ project, version }: { project: 'shlink' | 'shlink-web-client'; version: string }) => (
|
const VersionLink = ({ project, version }: { project: 'shlink' | 'shlink-web-client'; version: string }) => (
|
||||||
<ExternalLink href={`https://github.com/shlinkio/${project}/releases/${version}`} className="text-muted">
|
<ExternalLink href={`https://github.com/shlinkio/${project}/releases/${version}`} className="tw:text-gray-500">
|
||||||
<b>{version}</b>
|
<b>{version}</b>
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
);
|
);
|
||||||
@@ -21,7 +21,7 @@ export const ShlinkVersions = ({ selectedServer, clientVersion = SHLINK_WEB_CLIE
|
|||||||
const normalizedClientVersion = normalizeVersion(clientVersion);
|
const normalizedClientVersion = normalizeVersion(clientVersion);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<small className="text-muted">
|
<small className="tw:text-gray-500">
|
||||||
{isReachableServer(selectedServer) && (
|
{isReachableServer(selectedServer) && (
|
||||||
<>Server: <VersionLink project="shlink" version={selectedServer.printableVersion} /> - </>
|
<>Server: <VersionLink project="shlink" version={selectedServer.printableVersion} /> - </>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
@use '../../node_modules/@shlinkio/shlink-frontend-kit/dist/base';
|
|
||||||
|
|
||||||
.shlink-versions-container--with-sidebar {
|
|
||||||
margin-left: 0;
|
|
||||||
|
|
||||||
@media (min-width: base.$mdMin) {
|
|
||||||
margin-left: base.$asideMenuWidth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import { clsx } from 'clsx';
|
|||||||
import type { SelectedServer } from '../servers/data';
|
import type { SelectedServer } from '../servers/data';
|
||||||
import { isReachableServer } from '../servers/data';
|
import { isReachableServer } from '../servers/data';
|
||||||
import { ShlinkVersions } from './ShlinkVersions';
|
import { ShlinkVersions } from './ShlinkVersions';
|
||||||
import './ShlinkVersionsContainer.scss';
|
|
||||||
|
|
||||||
export type ShlinkVersionsContainerProps = {
|
export type ShlinkVersionsContainerProps = {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
@@ -10,9 +9,7 @@ export type ShlinkVersionsContainerProps = {
|
|||||||
|
|
||||||
export const ShlinkVersionsContainer = ({ selectedServer }: ShlinkVersionsContainerProps) => (
|
export const ShlinkVersionsContainer = ({ selectedServer }: ShlinkVersionsContainerProps) => (
|
||||||
<div
|
<div
|
||||||
className={clsx('text-center', {
|
className={clsx('tw:text-center', { 'tw:md:ml-(--aside-menu-width)': isReachableServer(selectedServer) })}
|
||||||
'shlink-versions-container--with-sidebar': isReachableServer(selectedServer),
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<ShlinkVersions selectedServer={selectedServer} />
|
<ShlinkVersions selectedServer={selectedServer} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import pack from '../package.json';
|
|||||||
import { container } from './container';
|
import { container } from './container';
|
||||||
import { setUpStore } from './container/store';
|
import { setUpStore } from './container/store';
|
||||||
import { register as registerServiceWorker } from './serviceWorkerRegistration';
|
import { register as registerServiceWorker } from './serviceWorkerRegistration';
|
||||||
|
import './tailwind.css';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
const store = setUpStore(container);
|
const store = setUpStore(container);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
|
import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
import { Result, useToggle } 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 { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
import type { FCWithDeps } from '../container/utils';
|
import type { FCWithDeps } from '../container/utils';
|
||||||
import { componentFactory, useDependencies } from '../container/utils';
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
@@ -26,11 +27,11 @@ type CreateServerDeps = {
|
|||||||
useTimeoutToggle: TimeoutToggle;
|
useTimeoutToggle: TimeoutToggle;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImportResult = ({ type }: { type: 'error' | 'success' }) => (
|
const ImportResult = ({ variant }: Pick<ResultProps, 'variant'>) => (
|
||||||
<div className="mt-3">
|
<div className="tw:mt-4">
|
||||||
<Result type={type}>
|
<Result variant={variant}>
|
||||||
{type === 'success' && 'Servers properly imported. You can now select one from the list :)'}
|
{variant === 'success' && 'Servers properly imported. You can now select one from the list :)'}
|
||||||
{type === 'error' && 'The servers could not be imported. Make sure the format is correct.'}
|
{variant === 'error' && 'The servers could not be imported. Make sure the format is correct.'}
|
||||||
</Result>
|
</Result>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -68,22 +69,22 @@ const CreateServer: FCWithDeps<CreateServerProps, CreateServerDeps> = ({ servers
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
<ServerForm title={<h5 className="mb-0">Add new server</h5>} onSubmit={onSubmit}>
|
<ServerForm title="Add new server" onSubmit={onSubmit}>
|
||||||
{!hasServers && (
|
{!hasServers && (
|
||||||
<ImportServersBtn tooltipPlacement="top" onImport={setServersImported} onImportError={setErrorImporting} />
|
<ImportServersBtn tooltipPlacement="top" onImport={setServersImported} onError={setErrorImporting} />
|
||||||
)}
|
)}
|
||||||
{hasServers && <Button outline onClick={goBack}>Cancel</Button>}
|
{hasServers && <Button type="button" variant="secondary" onClick={goBack}>Cancel</Button>}
|
||||||
<Button outline color="primary" className="ms-2">Create server</Button>
|
<Button type="submit">Create server</Button>
|
||||||
</ServerForm>
|
</ServerForm>
|
||||||
|
|
||||||
{serversImported && <ImportResult type="success" />}
|
{serversImported && <ImportResult variant="success" />}
|
||||||
{errorImporting && <ImportResult type="error" />}
|
{errorImporting && <ImportResult variant="error" />}
|
||||||
|
|
||||||
<DuplicatedServersModal
|
<DuplicatedServersModal
|
||||||
isOpen={isConfirmModalOpen}
|
open={isConfirmModalOpen}
|
||||||
duplicatedServers={serverData ? [serverData] : []}
|
duplicatedServers={serverData ? [serverData] : []}
|
||||||
onDiscard={goBack}
|
onClose={goBack}
|
||||||
onSave={() => serverData && saveNewServer(serverData)}
|
onConfirm={() => serverData && saveNewServer(serverData)}
|
||||||
/>
|
/>
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { useToggle } from '@shlinkio/shlink-frontend-kit';
|
import { useToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
import { clsx } from 'clsx';
|
|
||||||
import type { FC, PropsWithChildren } from 'react';
|
import type { FC, PropsWithChildren } from 'react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import type { FCWithDeps } from '../container/utils';
|
import type { FCWithDeps } from '../container/utils';
|
||||||
import { componentFactory, useDependencies } from '../container/utils';
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
import type { ServerWithId } from './data';
|
import type { ServerWithId } from './data';
|
||||||
@@ -10,28 +9,29 @@ import type { DeleteServerModalProps } from './DeleteServerModal';
|
|||||||
|
|
||||||
export type DeleteServerButtonProps = PropsWithChildren<{
|
export type DeleteServerButtonProps = PropsWithChildren<{
|
||||||
server: ServerWithId;
|
server: ServerWithId;
|
||||||
className?: string;
|
|
||||||
textClassName?: string;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type DeleteServerButtonDeps = {
|
type DeleteServerButtonDeps = {
|
||||||
DeleteServerModal: FC<DeleteServerModalProps>;
|
DeleteServerModal: FC<DeleteServerModalProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeleteServerButton: FCWithDeps<DeleteServerButtonProps, DeleteServerButtonDeps> = (
|
const DeleteServerButton: FCWithDeps<DeleteServerButtonProps, DeleteServerButtonDeps> = ({ server, children }) => {
|
||||||
{ server, className, children, textClassName },
|
|
||||||
) => {
|
|
||||||
const { DeleteServerModal } = useDependencies(DeleteServerButton);
|
const { DeleteServerModal } = useDependencies(DeleteServerButton);
|
||||||
const [isModalOpen, , showModal, hideModal] = useToggle();
|
const [isModalOpen, , showModal, hideModal] = useToggle();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const onClose = useCallback((confirmed: boolean) => {
|
||||||
|
hideModal();
|
||||||
|
if (confirmed) {
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
}, [hideModal, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button type="button" className={clsx(className, 'p-0 bg-transparent border-0')} onClick={showModal}>
|
<button type="button" className="tw:text-danger tw:hover:underline" onClick={showModal}>
|
||||||
{!children && <FontAwesomeIcon fixedWidth icon={deleteIcon} />}
|
{children}
|
||||||
<span className={textClassName}>{children ?? 'Remove this server'}</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
<DeleteServerModal server={server} open={isModalOpen} onClose={onClose} />
|
||||||
<DeleteServerModal server={server} isOpen={isModalOpen} toggle={hideModal} />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,44 +1,37 @@
|
|||||||
|
import type { ExitAction } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
|
import { CardModal } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useRef } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
|
||||||
import type { ServerWithId } from './data';
|
import type { ServerWithId } from './data';
|
||||||
|
|
||||||
export interface DeleteServerModalProps {
|
export type DeleteServerModalProps = {
|
||||||
server: ServerWithId;
|
server: ServerWithId;
|
||||||
toggle: () => void;
|
onClose: (confirmed: boolean) => void;
|
||||||
isOpen: boolean;
|
open: boolean;
|
||||||
redirectHome?: boolean;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteServerModalConnectProps extends DeleteServerModalProps {
|
type DeleteServerModalConnectProps = DeleteServerModalProps & {
|
||||||
deleteServer: (server: ServerWithId) => void;
|
deleteServer: (server: ServerWithId) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
|
|
||||||
{ server, toggle, isOpen, deleteServer, redirectHome = true },
|
|
||||||
) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const doDelete = useRef<boolean>(false);
|
|
||||||
const toggleAndDelete = () => {
|
|
||||||
doDelete.current = true;
|
|
||||||
toggle();
|
|
||||||
};
|
|
||||||
const onClosed = () => {
|
|
||||||
if (!doDelete.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export const DeleteServerModal: FC<DeleteServerModalConnectProps> = ({ server, onClose, open, deleteServer }) => {
|
||||||
|
const onClosed = useCallback((exitAction: ExitAction) => {
|
||||||
|
if (exitAction === 'confirm') {
|
||||||
deleteServer(server);
|
deleteServer(server);
|
||||||
if (redirectHome) {
|
|
||||||
navigate('/');
|
|
||||||
}
|
}
|
||||||
};
|
}, [deleteServer, server]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered onClosed={onClosed}>
|
<CardModal
|
||||||
<ModalHeader toggle={toggle} className="text-danger">Remove server</ModalHeader>
|
open={open}
|
||||||
<ModalBody>
|
title="Remove server"
|
||||||
|
variant="danger"
|
||||||
|
onClose={() => onClose(false)}
|
||||||
|
onConfirm={() => onClose(true)}
|
||||||
|
onClosed={onClosed}
|
||||||
|
confirmText="Delete"
|
||||||
|
>
|
||||||
|
<div className="tw:flex tw:flex-col tw:gap-y-4">
|
||||||
<p>Are you sure you want to remove <b>{server ? server.name : ''}</b>?</p>
|
<p>Are you sure you want to remove <b>{server ? server.name : ''}</b>?</p>
|
||||||
<p>
|
<p>
|
||||||
<i>
|
<i>
|
||||||
@@ -46,11 +39,7 @@ export const DeleteServerModal: FC<DeleteServerModalConnectProps> = (
|
|||||||
You can create it again at any moment.
|
You can create it again at any moment.
|
||||||
</i>
|
</i>
|
||||||
</p>
|
</p>
|
||||||
</ModalBody>
|
</div>
|
||||||
<ModalFooter>
|
</CardModal>
|
||||||
<Button color="link" onClick={toggle}>Cancel</Button>
|
|
||||||
<Button color="danger" onClick={toggleAndDelete}>Delete</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useParsedQuery } from '@shlinkio/shlink-frontend-kit';
|
import { useParsedQuery } from '@shlinkio/shlink-frontend-kit';
|
||||||
|
import { Button } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
import type { FCWithDeps } from '../container/utils';
|
import type { FCWithDeps } from '../container/utils';
|
||||||
import { componentFactory } from '../container/utils';
|
import { componentFactory } from '../container/utils';
|
||||||
@@ -40,12 +40,12 @@ const EditServer: FCWithDeps<EditServerProps, EditServerDeps> = withSelectedServ
|
|||||||
return (
|
return (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
<ServerForm
|
<ServerForm
|
||||||
title={<h5 className="mb-0">Edit "{selectedServer.name}"</h5>}
|
title={<>Edit "{selectedServer.name}"</>}
|
||||||
initialValues={selectedServer}
|
initialValues={selectedServer}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
<Button outline className="me-2" onClick={goBack}>Cancel</Button>
|
<Button type="button" variant="secondary" onClick={goBack}>Cancel</Button>
|
||||||
<Button outline color="primary">Save</Button>
|
<Button>Save</Button>
|
||||||
</ServerForm>
|
</ServerForm>
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { faFileDownload as exportIcon, faPlus as plusIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faFileDownload as exportIcon, faPlus as plusIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
|
import type { TimeoutToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
import { Result, SearchField, SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
import { Button, Result, SearchInput, SimpleCard, Table } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Link } from 'react-router';
|
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||||
import type { FCWithDeps } from '../container/utils';
|
import type { FCWithDeps } from '../container/utils';
|
||||||
import { componentFactory, useDependencies } from '../container/utils';
|
import { componentFactory, useDependencies } from '../container/utils';
|
||||||
@@ -45,45 +43,46 @@ const ManageServers: FCWithDeps<ManageServersProps, ManageServersDeps> = ({ serv
|
|||||||
const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
|
const [errorImporting, setErrorImporting] = useTimeoutToggle(false, SHOW_IMPORT_MSG_TIME);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NoMenuLayout className="d-flex flex-column gap-3">
|
<NoMenuLayout className="tw:flex tw:flex-col tw:gap-y-4">
|
||||||
<SearchField onChange={setSearchTerm} />
|
<SearchInput onChange={setSearchTerm} />
|
||||||
|
|
||||||
<div className="d-flex flex-column flex-md-row gap-2">
|
<div className="tw:flex tw:flex-col tw:md:flex-row tw:gap-2">
|
||||||
<div className="d-flex gap-2">
|
<div className="tw:flex tw:gap-2">
|
||||||
<ImportServersBtn className="flex-fill" onImportError={setErrorImporting}>Import servers</ImportServersBtn>
|
<ImportServersBtn className="tw:flex-grow" onError={setErrorImporting}>Import servers</ImportServersBtn>
|
||||||
{filteredServers.length > 0 && (
|
{filteredServers.length > 0 && (
|
||||||
<Button outline className="flex-fill" onClick={async () => serversExporter.exportServers()}>
|
<Button variant="secondary" className="tw:flex-grow" onClick={async () => serversExporter.exportServers()}>
|
||||||
<FontAwesomeIcon icon={exportIcon} fixedWidth /> Export servers
|
<FontAwesomeIcon icon={exportIcon} /> Export servers
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button outline color="primary" className="ms-md-auto" tag={Link} to="/server/create">
|
<Button className="tw:md:ml-auto" to="/server/create">
|
||||||
<FontAwesomeIcon icon={plusIcon} fixedWidth /> Add a server
|
<FontAwesomeIcon icon={plusIcon} /> Add a server
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SimpleCard>
|
<SimpleCard className="card">
|
||||||
<table className="table table-hover responsive-table mb-0">
|
<Table header={(
|
||||||
<thead className="responsive-table__header">
|
<Table.Row>
|
||||||
<tr>
|
{hasAutoConnect && (
|
||||||
{hasAutoConnect && <th style={{ width: '50px' }}><span className="sr-only">Auto-connect</span></th>}
|
<Table.Cell className="tw:w-[35px]"><span className="tw:sr-only">Auto-connect</span></Table.Cell>
|
||||||
<th>Name</th>
|
)}
|
||||||
<th>Base URL</th>
|
<Table.Cell>Name</Table.Cell>
|
||||||
<th><span className="sr-only">Options</span></th>
|
<Table.Cell>Base URL</Table.Cell>
|
||||||
</tr>
|
<Table.Cell><span className="sr-only">Options</span></Table.Cell>
|
||||||
</thead>
|
</Table.Row>
|
||||||
<tbody>
|
)}>
|
||||||
{!filteredServers.length && <tr className="text-center"><td colSpan={4}>No servers found.</td></tr>}
|
{!filteredServers.length && (
|
||||||
|
<Table.Row className="tw:text-center"><Table.Cell colSpan={4}>No servers found.</Table.Cell></Table.Row>
|
||||||
|
)}
|
||||||
{filteredServers.map((server) => (
|
{filteredServers.map((server) => (
|
||||||
<ManageServersRow key={server.id} server={server} hasAutoConnect={hasAutoConnect} />
|
<ManageServersRow key={server.id} server={server} hasAutoConnect={hasAutoConnect} />
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</Table>
|
||||||
</table>
|
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
|
|
||||||
{errorImporting && (
|
{errorImporting && (
|
||||||
<div>
|
<div>
|
||||||
<Result type="error">The servers could not be imported. Make sure the format is correct.</Result>
|
<Result variant="error">The servers could not be imported. Make sure the format is correct.</Result>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { faCheck as checkIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faCheck as checkIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { Table } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
@@ -21,27 +22,27 @@ const ManageServersRow: FCWithDeps<ManageServersRowProps, ManageServersRowDeps>
|
|||||||
const { ManageServersRowDropdown } = useDependencies(ManageServersRow);
|
const { ManageServersRowDropdown } = useDependencies(ManageServersRow);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="responsive-table__row">
|
<Table.Row className="tw:relative">
|
||||||
{hasAutoConnect && (
|
{hasAutoConnect && (
|
||||||
<td className="responsive-table__cell" data-th="Auto-connect">
|
<Table.Cell columnName="Auto-connect">
|
||||||
{server.autoConnect && (
|
{server.autoConnect && (
|
||||||
<>
|
<>
|
||||||
<FontAwesomeIcon icon={checkIcon} className="text-primary" id="autoConnectIcon" />
|
<FontAwesomeIcon icon={checkIcon} className="tw:text-brand" id="autoConnectIcon" />
|
||||||
<UncontrolledTooltip target="autoConnectIcon" placement="right">
|
<UncontrolledTooltip target="autoConnectIcon" placement="right">
|
||||||
Auto-connect to this server
|
Auto-connect to this server
|
||||||
</UncontrolledTooltip>
|
</UncontrolledTooltip>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</td>
|
</Table.Cell>
|
||||||
)}
|
)}
|
||||||
<th className="responsive-table__cell" data-th="Name">
|
<Table.Cell className="tw:font-bold" columnName="Name">
|
||||||
<Link to={`/server/${server.id}`}>{server.name}</Link>
|
<Link to={`/server/${server.id}`}>{server.name}</Link>
|
||||||
</th>
|
</Table.Cell>
|
||||||
<td className="responsive-table__cell" data-th="Base URL">{server.url}</td>
|
<Table.Cell columnName="Base URL" className="tw:max-lg:border-b-0">{server.url}</Table.Cell>
|
||||||
<td className="responsive-table__cell text-end">
|
<Table.Cell className="tw:text-right tw:max-lg:absolute tw:right-0 tw:-top-1 tw:mx-lg:pt-0">
|
||||||
<ManageServersRowDropdown server={server} />
|
<ManageServersRowDropdown server={server} />
|
||||||
</td>
|
</Table.Cell>
|
||||||
</tr>
|
</Table.Row>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ const ManageServersRowDropdown: FCWithDeps<ManageServersRowDropdownConnectProps,
|
|||||||
<FontAwesomeIcon icon={autoConnectIcon} fixedWidth /> {isAutoConnect ? 'Do not a' : 'A'}uto-connect
|
<FontAwesomeIcon icon={autoConnectIcon} fixedWidth /> {isAutoConnect ? 'Do not a' : 'A'}uto-connect
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem divider tag="hr" />
|
<DropdownItem divider tag="hr" />
|
||||||
<DropdownItem className="dropdown-item--danger" onClick={showModal}>
|
<DropdownItem className="tw:text-danger" onClick={showModal}>
|
||||||
<FontAwesomeIcon icon={deleteIcon} fixedWidth /> Remove server
|
<FontAwesomeIcon icon={deleteIcon} fixedWidth /> Remove server
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|
||||||
<DeleteServerModal redirectHome={false} server={server} isOpen={isModalOpen} toggle={hideModal} />
|
<DeleteServerModal server={server} open={isModalOpen} onClose={hideModal} />
|
||||||
</RowDropdownBtn>
|
</RowDropdownBtn>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProp
|
|||||||
if (serversList.length === 0) {
|
if (serversList.length === 0) {
|
||||||
return (
|
return (
|
||||||
<DropdownItem tag={Link} to="/server/create">
|
<DropdownItem tag={Link} to="/server/create">
|
||||||
<FontAwesomeIcon icon={plusIcon} /> <span className="ms-1">Add a server</span>
|
<FontAwesomeIcon icon={plusIcon} /> <span className="tw:ml-1">Add a server</span>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProp
|
|||||||
))}
|
))}
|
||||||
<DropdownItem divider tag="hr" />
|
<DropdownItem divider tag="hr" />
|
||||||
<DropdownItem tag={Link} to="/manage-servers">
|
<DropdownItem tag={Link} to="/manage-servers">
|
||||||
<FontAwesomeIcon icon={serverIcon} /> <span className="ms-1">Manage servers</span>
|
<FontAwesomeIcon icon={serverIcon} /> <span className="tw:ml-1">Manage servers</span>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -40,9 +40,9 @@ export const ServersDropdown = ({ servers, selectedServer }: ServersDropdownProp
|
|||||||
return (
|
return (
|
||||||
<UncontrolledDropdown nav inNavbar>
|
<UncontrolledDropdown nav inNavbar>
|
||||||
<DropdownToggle nav caret>
|
<DropdownToggle nav caret>
|
||||||
<FontAwesomeIcon icon={serverIcon} /> <span className="ms-1">Servers</span>
|
<FontAwesomeIcon icon={serverIcon} /> <span className="tw:ml-1">Servers</span>
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenu end style={{ right: 0 }}>{renderServers()}</DropdownMenu>
|
<DropdownMenu end classNam="tw:right-0">{renderServers()}</DropdownMenu>
|
||||||
</UncontrolledDropdown>
|
</UncontrolledDropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
@use '../../node_modules/@shlinkio/shlink-frontend-kit/dist/base';
|
|
||||||
@use '../utils/mixins/vertical-align';
|
|
||||||
@use '../utils/mixins/thin-scroll';
|
|
||||||
|
|
||||||
.servers-list__list-group.servers-list__list-group {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.servers-list__list-group:not(.servers-list__list-group--embedded) {
|
|
||||||
max-width: 400px;
|
|
||||||
box-shadow: 0 .125rem .25rem rgb(0 0 0 / .075);
|
|
||||||
}
|
|
||||||
|
|
||||||
.servers-list__server-item.servers-list__server-item {
|
|
||||||
text-align: left;
|
|
||||||
position: relative;
|
|
||||||
padding: .75rem 2.5rem .75rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.servers-list__server-item:not(:hover) {
|
|
||||||
color: base.$mainColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.servers-list__server-item:hover {
|
|
||||||
background-color: var(--secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.servers-list__server-item-icon {
|
|
||||||
@include vertical-align.vertical-align();
|
|
||||||
|
|
||||||
right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.servers-list__list-group--embedded.servers-list__list-group--embedded {
|
|
||||||
border-radius: 0;
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
|
|
||||||
@media (min-width: base.$mdMin) {
|
|
||||||
max-height: 220px;
|
|
||||||
overflow-x: auto;
|
|
||||||
|
|
||||||
@include thin-scroll.thin-scroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
.servers-list__server-item {
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +1,43 @@
|
|||||||
import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import type { FC, PropsWithChildren } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { ListGroup, ListGroupItem } from 'reactstrap';
|
|
||||||
import type { ServerWithId } from './data';
|
import type { ServerWithId } from './data';
|
||||||
import './ServersListGroup.scss';
|
|
||||||
|
|
||||||
type ServersListGroupProps = PropsWithChildren<{
|
type ServersListGroupProps = {
|
||||||
servers: ServerWithId[];
|
servers: ServerWithId[];
|
||||||
embedded?: boolean;
|
borderless?: boolean;
|
||||||
}>;
|
};
|
||||||
|
|
||||||
const ServerListItem = ({ id, name }: { id: string; name: string }) => (
|
const ServerListItem = ({ id, name }: { id: string; name: string }) => (
|
||||||
<ListGroupItem tag={Link} to={`/server/${id}`} className="servers-list__server-item">
|
<Link
|
||||||
{name}
|
to={`/server/${id}`}
|
||||||
<FontAwesomeIcon icon={chevronIcon} className="servers-list__server-item-icon" />
|
className={clsx(
|
||||||
</ListGroupItem>
|
'servers-list__server-item',
|
||||||
|
'tw:flex tw:items-center tw:justify-between tw:gap-x-2 tw:px-4 tw:py-3',
|
||||||
|
'tw:rounded-none tw:hover:bg-lm-secondary tw:hover:dark:bg-dm-secondary',
|
||||||
|
'tw:border-b tw:last:border-0 tw:border-lm-border tw:dark:border-dm-border',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="tw:truncate">{name}</span>
|
||||||
|
<FontAwesomeIcon icon={chevronIcon} />
|
||||||
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ServersListGroup: FC<ServersListGroupProps> = ({ servers, children, embedded = false }) => (
|
export const ServersListGroup: FC<ServersListGroupProps> = ({ servers, borderless }) => (
|
||||||
<>
|
<>
|
||||||
{children && <div data-testid="title" className="mb-0 fs-5 fw-normal lh-sm">{children}</div>}
|
|
||||||
{servers.length > 0 && (
|
{servers.length > 0 && (
|
||||||
<ListGroup
|
<div
|
||||||
data-testid="list"
|
data-testid="list"
|
||||||
tag="div"
|
className={clsx(
|
||||||
className={clsx('servers-list__list-group', { 'servers-list__list-group--embedded': embedded })}
|
'tw:w-full tw:border-lm-border tw:dark:border-dm-border',
|
||||||
|
'tw:md:max-h-56 tw:md:overflow-y-auto tw:-mb-1 tw:scroll-thin',
|
||||||
|
{ 'tw:border-y': !borderless },
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{servers.map(({ id, name }) => <ServerListItem key={id} id={id} name={name} />)}
|
{servers.map(({ id, name }) => <ServerListItem key={id} id={id} name={name} />)}
|
||||||
</ListGroup>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
|
import { CardModal } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
|
||||||
import type { ServerData } from '../data';
|
import type { ServerData } from '../data';
|
||||||
|
|
||||||
interface DuplicatedServersModalProps {
|
export type DuplicatedServersModalProps = {
|
||||||
duplicatedServers: ServerData[];
|
duplicatedServers: ServerData[];
|
||||||
isOpen: boolean;
|
open: boolean;
|
||||||
onDiscard: () => void;
|
onClose: () => void;
|
||||||
onSave: () => void;
|
onConfirm: () => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const DuplicatedServersModal: FC<DuplicatedServersModalProps> = (
|
export const DuplicatedServersModal: FC<DuplicatedServersModalProps> = (
|
||||||
{ isOpen, duplicatedServers, onDiscard, onSave },
|
{ open, duplicatedServers, onClose, onConfirm },
|
||||||
) => {
|
) => {
|
||||||
const hasMultipleServers = duplicatedServers.length > 1;
|
const hasMultipleServers = duplicatedServers.length > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal centered isOpen={isOpen}>
|
<CardModal
|
||||||
<ModalHeader>Duplicated server{hasMultipleServers && 's'}</ModalHeader>
|
size="lg"
|
||||||
<ModalBody>
|
title={`Duplicated server${hasMultipleServers ? 's' : ''}`}
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
confirmText={`Save duplicate${hasMultipleServers ? 's' : ''}`}
|
||||||
|
cancelText={hasMultipleServers ? 'Ignore duplicates' : 'Discard'}
|
||||||
|
>
|
||||||
<p>{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}</p>
|
<p>{hasMultipleServers ? 'The next servers already exist:' : 'There is already a server with:'}</p>
|
||||||
<ul>
|
<ul className="tw:list-disc tw:mt-4">
|
||||||
{duplicatedServers.map(({ url, apiKey }, index) => (!hasMultipleServers ? (
|
{duplicatedServers.map(({ url, apiKey }, index) => (!hasMultipleServers ? (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
<li>URL: <b>{url}</b></li>
|
<li>URL: <b>{url}</b></li>
|
||||||
@@ -29,13 +35,8 @@ export const DuplicatedServersModal: FC<DuplicatedServersModalProps> = (
|
|||||||
) : <li key={index}><b>{url}</b> - <b>{apiKey}</b></li>))}
|
) : <li key={index}><b>{url}</b> - <b>{apiKey}</b></li>))}
|
||||||
</ul>
|
</ul>
|
||||||
<span>
|
<span>
|
||||||
{hasMultipleServers ? 'Do you want to ignore duplicated servers' : 'Do you want to save this server anyway'}?
|
{hasMultipleServers ? 'Do you want to save duplicated servers' : 'Do you want to save this server'}?
|
||||||
</span>
|
</span>
|
||||||
</ModalBody>
|
</CardModal>
|
||||||
<ModalFooter>
|
|
||||||
<Button color="link" onClick={onDiscard}>{hasMultipleServers ? 'Ignore duplicates' : 'Discard'}</Button>
|
|
||||||
<Button color="primary" onClick={onSave}>Save anyway</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</Modal>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
|
import { faFileUpload as importIcon } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { useElementRef, useToggle } from '@shlinkio/shlink-frontend-kit';
|
import { useElementRef, useToggle } from '@shlinkio/shlink-frontend-kit';
|
||||||
|
import { Button } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import type { ChangeEvent, PropsWithChildren } from 'react';
|
import type { ChangeEvent, PropsWithChildren } from 'react';
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useRef , useState } from 'react';
|
||||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
import { UncontrolledTooltip } from 'reactstrap';
|
||||||
import type { FCWithDeps } from '../../container/utils';
|
import type { FCWithDeps } from '../../container/utils';
|
||||||
import { componentFactory, useDependencies } from '../../container/utils';
|
import { componentFactory, useDependencies } from '../../container/utils';
|
||||||
import type { ServerData, ServersMap, ServerWithId } from '../data';
|
import type { ServerData, ServersMap, ServerWithId } from '../data';
|
||||||
@@ -13,7 +14,7 @@ import { dedupServers, ensureUniqueIds } from './index';
|
|||||||
|
|
||||||
export type ImportServersBtnProps = PropsWithChildren<{
|
export type ImportServersBtnProps = PropsWithChildren<{
|
||||||
onImport?: () => void;
|
onImport?: () => void;
|
||||||
onImportError?: (error: Error) => void;
|
onError?: (error: Error) => void;
|
||||||
tooltipPlacement?: 'top' | 'bottom';
|
tooltipPlacement?: 'top' | 'bottom';
|
||||||
className?: string;
|
className?: string;
|
||||||
}>;
|
}>;
|
||||||
@@ -31,8 +32,8 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
|
|||||||
createServers,
|
createServers,
|
||||||
servers,
|
servers,
|
||||||
children,
|
children,
|
||||||
onImport = () => {},
|
onImport,
|
||||||
onImportError = () => {},
|
onError = () => {},
|
||||||
tooltipPlacement = 'bottom',
|
tooltipPlacement = 'bottom',
|
||||||
className = '',
|
className = '',
|
||||||
}) => {
|
}) => {
|
||||||
@@ -40,50 +41,56 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
|
|||||||
const ref = useElementRef<HTMLInputElement>();
|
const ref = useElementRef<HTMLInputElement>();
|
||||||
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
|
const [duplicatedServers, setDuplicatedServers] = useState<ServerData[]>([]);
|
||||||
const [isModalOpen,, showModal, hideModal] = useToggle();
|
const [isModalOpen,, showModal, hideModal] = useToggle();
|
||||||
|
const newServersCreatedRef = useRef(false);
|
||||||
|
|
||||||
const importedServersRef = useRef<ServerWithId[]>([]);
|
|
||||||
const newServersRef = useRef<ServerWithId[]>([]);
|
|
||||||
|
|
||||||
const create = useCallback((serversData: ServerWithId[]) => {
|
|
||||||
createServers(serversData);
|
|
||||||
onImport();
|
|
||||||
}, [createServers, onImport]);
|
|
||||||
const onFile = useCallback(
|
const onFile = useCallback(
|
||||||
async ({ target }: ChangeEvent<HTMLInputElement>) =>
|
async ({ target }: ChangeEvent<HTMLInputElement>) =>
|
||||||
serversImporter.importServersFromFile(target.files?.[0])
|
serversImporter.importServersFromFile(target.files?.[0])
|
||||||
.then((importedServers) => {
|
.then((importedServers) => {
|
||||||
const { duplicatedServers, newServers } = dedupServers(servers, importedServers);
|
const { duplicatedServers, newServers } = dedupServers(servers, importedServers);
|
||||||
|
|
||||||
importedServersRef.current = ensureUniqueIds(servers, importedServers);
|
// Immediately create new servers
|
||||||
newServersRef.current = ensureUniqueIds(servers, newServers);
|
newServersCreatedRef.current = newServers.length > 0;
|
||||||
|
createServers(ensureUniqueIds(servers, newServers));
|
||||||
|
|
||||||
if (duplicatedServers.length === 0) {
|
// For duplicated servers, ask for confirmation
|
||||||
create(importedServersRef.current);
|
if (duplicatedServers.length > 0) {
|
||||||
} else {
|
|
||||||
setDuplicatedServers(duplicatedServers);
|
setDuplicatedServers(duplicatedServers);
|
||||||
showModal();
|
showModal();
|
||||||
|
} else {
|
||||||
|
onImport?.();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Reset file input after processing file
|
// Reset file input after processing file
|
||||||
(target as { value: string | null }).value = null;
|
(target as { value: string | null }).value = null;
|
||||||
})
|
})
|
||||||
.catch(onImportError),
|
.catch(onError),
|
||||||
[create, onImportError, servers, serversImporter, showModal],
|
[createServers, onError, onImport, servers, serversImporter, showModal],
|
||||||
);
|
);
|
||||||
|
|
||||||
const createAllServers = useCallback(() => {
|
const createDuplicatedServers = useCallback(() => {
|
||||||
create(importedServersRef.current);
|
createServers(ensureUniqueIds(servers, duplicatedServers));
|
||||||
hideModal();
|
hideModal();
|
||||||
}, [create, hideModal]);
|
onImport?.();
|
||||||
const createNonDuplicatedServers = useCallback(() => {
|
}, [createServers, duplicatedServers, hideModal, onImport, servers]);
|
||||||
create(newServersRef.current);
|
const discardDuplicatedServers = useCallback(() => {
|
||||||
hideModal();
|
hideModal();
|
||||||
}, [create, hideModal]);
|
// If duplicated servers were discarded but some non-duplicated servers were created, call onImport
|
||||||
|
if (newServersCreatedRef.current) {
|
||||||
|
onImport?.();
|
||||||
|
}
|
||||||
|
}, [hideModal, onImport]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button outline id="importBtn" className={className} onClick={() => ref.current?.click()}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
id="importBtn"
|
||||||
|
className={className}
|
||||||
|
onClick={() => ref.current?.click()}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={importIcon} fixedWidth /> {children ?? 'Import from file'}
|
<FontAwesomeIcon icon={importIcon} fixedWidth /> {children ?? 'Import from file'}
|
||||||
</Button>
|
</Button>
|
||||||
<UncontrolledTooltip placement={tooltipPlacement} target="importBtn">
|
<UncontrolledTooltip placement={tooltipPlacement} target="importBtn">
|
||||||
@@ -93,7 +100,7 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
|
|||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".csv"
|
accept=".csv"
|
||||||
className="d-none"
|
className="tw:hidden"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
ref={ref as any /* TODO Remove After updating to React 19 */}
|
ref={ref as any /* TODO Remove After updating to React 19 */}
|
||||||
onChange={onFile}
|
onChange={onFile}
|
||||||
@@ -101,10 +108,10 @@ const ImportServersBtn: FCWithDeps<ImportServersBtnConnectProps, ImportServersBt
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<DuplicatedServersModal
|
<DuplicatedServersModal
|
||||||
isOpen={isModalOpen}
|
open={isModalOpen}
|
||||||
duplicatedServers={duplicatedServers}
|
duplicatedServers={duplicatedServers}
|
||||||
onDiscard={createNonDuplicatedServers}
|
onClose={discardDuplicatedServers}
|
||||||
onSave={createAllServers}
|
onConfirm={createDuplicatedServers}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
@use '../../../node_modules/@shlinkio/shlink-frontend-kit/dist/base';
|
|
||||||
|
|
||||||
.server-error__container {
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-error__delete-btn {
|
|
||||||
color: base.$dangerColor;
|
|
||||||
font-weight: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-error__delete-btn:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Message } from '@shlinkio/shlink-frontend-kit';
|
import { Card, Message } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
||||||
@@ -8,7 +8,6 @@ import type { SelectedServer, ServersMap } from '../data';
|
|||||||
import { isServerWithId } from '../data';
|
import { isServerWithId } from '../data';
|
||||||
import type { DeleteServerButtonProps } from '../DeleteServerButton';
|
import type { DeleteServerButtonProps } from '../DeleteServerButton';
|
||||||
import { ServersListGroup } from '../ServersListGroup';
|
import { ServersListGroup } from '../ServersListGroup';
|
||||||
import './ServerError.scss';
|
|
||||||
|
|
||||||
type ServerErrorProps = {
|
type ServerErrorProps = {
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
@@ -24,8 +23,8 @@ const ServerError: FCWithDeps<ServerErrorProps, ServerErrorDeps> = ({ servers, s
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NoMenuLayout>
|
<NoMenuLayout>
|
||||||
<div className="server-error__container flex-column">
|
<div className="tw:flex tw:flex-col tw:items-center tw:gap-y-4 tw:md:gap-y-8">
|
||||||
<Message className="w-100 mb-3 mb-md-5" type="error" fullWidth>
|
<Message className="tw:w-full tw:lg:w-[80%]" variant="error">
|
||||||
{!isServerWithId(selectedServer) && 'Could not find this Shlink server.'}
|
{!isServerWithId(selectedServer) && 'Could not find this Shlink server.'}
|
||||||
{isServerWithId(selectedServer) && (
|
{isServerWithId(selectedServer) && (
|
||||||
<>
|
<>
|
||||||
@@ -35,21 +34,21 @@ const ServerError: FCWithDeps<ServerErrorProps, ServerErrorDeps> = ({ servers, s
|
|||||||
)}
|
)}
|
||||||
</Message>
|
</Message>
|
||||||
|
|
||||||
<ServersListGroup servers={Object.values(servers)}>
|
<p className="tw:text-xl">
|
||||||
<p className="mb-md-3">
|
|
||||||
These are the Shlink servers currently configured. Choose one of
|
These are the Shlink servers currently configured. Choose one of
|
||||||
them or <Link to="/server/create">add a new one</Link>.
|
them or <Link to="/server/create">add a new one</Link>.
|
||||||
</p>
|
</p>
|
||||||
</ServersListGroup>
|
<Card className="tw:w-full tw:max-w-100 tw:overflow-hidden">
|
||||||
|
<ServersListGroup borderless servers={Object.values(servers)} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
{isServerWithId(selectedServer) && (
|
{isServerWithId(selectedServer) && (
|
||||||
<div className="container mt-3 mt-md-5">
|
<p className="tw:text-xl">
|
||||||
<p className="fs-5 fw-normal lh-sm">
|
|
||||||
Alternatively, if you think you may have misconfigured this server, you
|
Alternatively, if you think you may have misconfigured this server, you
|
||||||
can <DeleteServerButton server={selectedServer} className="server-error__delete-btn">remove it</DeleteServerButton> or
|
can <DeleteServerButton server={selectedServer}>remove
|
||||||
|
it</DeleteServerButton> or
|
||||||
<Link to={`/server/${selectedServer.id}/edit?reconnect=true`}>edit it</Link>.
|
<Link to={`/server/${selectedServer.id}/edit?reconnect=true`}>edit it</Link>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</NoMenuLayout>
|
</NoMenuLayout>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InputFormGroup, SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
import { LabelledInput, SimpleCard } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { handleEventPreventingDefault } from '../../utils/utils';
|
import { handleEventPreventingDefault } from '../../utils/utils';
|
||||||
@@ -17,14 +17,20 @@ export const ServerForm: FC<ServerFormProps> = ({ onSubmit, initialValues, child
|
|||||||
const handleSubmit = handleEventPreventingDefault(() => onSubmit({ name, url, apiKey }));
|
const handleSubmit = handleEventPreventingDefault(() => onSubmit({ name, url, apiKey }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="server-form" name="serverForm" onSubmit={handleSubmit}>
|
<form name="serverForm" onSubmit={handleSubmit}>
|
||||||
<SimpleCard className="mb-3" title={title}>
|
<SimpleCard className="tw:mb-4" bodyClassName="tw:flex tw:flex-col tw:gap-y-3" title={title}>
|
||||||
<InputFormGroup value={name} onChange={setName}>Name</InputFormGroup>
|
<LabelledInput label="Name" value={name} onChange={(e) => setName(e.target.value)} required />
|
||||||
<InputFormGroup type="url" value={url} onChange={setUrl}>URL</InputFormGroup>
|
<LabelledInput label="URL" type="url" value={url} onChange={(e) => setUrl(e.target.value)} required />
|
||||||
<InputFormGroup value={apiKey} onChange={setApiKey}>API key</InputFormGroup>
|
<LabelledInput
|
||||||
|
label="API key"
|
||||||
|
type="password"
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</SimpleCard>
|
</SimpleCard>
|
||||||
|
|
||||||
<div className="text-end">{children}</div>
|
<div className="tw:flex tw:items-center tw:justify-end tw:gap-x-2">{children}</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Message } from '@shlinkio/shlink-frontend-kit';
|
import { Message } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|||||||
27
src/tailwind.css
Normal file
27
src/tailwind.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
@import 'tailwindcss' prefix(tw) important;
|
||||||
|
@source '../node_modules/@shlinkio/shlink-frontend-kit';
|
||||||
|
@import '@shlinkio/shlink-frontend-kit/tailwind.preset.css';
|
||||||
|
|
||||||
|
@utility scroll-thin {
|
||||||
|
/* Standard. New browsers */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
|
||||||
|
/* Fallback */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
@mixin horizontal-align {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
@mixin thin-scroll() {
|
|
||||||
/* Forefox scrollbar */
|
|
||||||
scrollbar-color: rgba(0, 0, 0, .2) #f5f5f5;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
|
|
||||||
/* Chrome webkit scrollbar */
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba(0, 0, 0, .2);
|
|
||||||
border-radius: .5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
@mixin vertical-align($extraTransforms: null) {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%) $extraTransforms;
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,24 @@
|
|||||||
import { useToggle } from '@shlinkio/shlink-frontend-kit';
|
|
||||||
import type { FC, ReactElement } from 'react';
|
import type { FC, ReactElement } from 'react';
|
||||||
|
import { useCallback, useEffect , useState } from 'react';
|
||||||
|
|
||||||
interface RenderModalArgs {
|
export type RenderModalArgs = {
|
||||||
isOpen: boolean;
|
open: boolean;
|
||||||
toggle: () => void;
|
onClose: () => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const TestModalWrapper: FC<{ renderModal: (args: RenderModalArgs) => ReactElement }> = (
|
export const TestModalWrapper: FC<{ renderModal: (args: RenderModalArgs) => ReactElement }> = (
|
||||||
{ renderModal },
|
{ renderModal },
|
||||||
) => {
|
) => {
|
||||||
const [isOpen, toggle] = useToggle(true);
|
const [open, setOpen] = useState(true);
|
||||||
return renderModal({ isOpen, toggle });
|
const onClose = useCallback(() => setOpen(false), []);
|
||||||
|
|
||||||
|
// Workaround to ensure CardModals from shlink-frontend-shared can be closed, as they depend on CSS transitions
|
||||||
|
// Since JSDOM does not support them, this dispatches the event right after the listener has been set-up
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
document.querySelector('[data-testid="transition-container"]')?.dispatchEvent(new Event('transitionend'));
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return renderModal({ open, onClose });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,13 +54,17 @@ describe('<App />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['/foo', 'shlink-wrapper'],
|
['/foo', false],
|
||||||
['/bar', 'shlink-wrapper'],
|
['/bar', false],
|
||||||
['/', 'shlink-wrapper d-flex align-items-center pt-3'],
|
['/', true],
|
||||||
])('renders expected classes on shlink-wrapper based on current pathname', async (pathname, expectedClasses) => {
|
])('renders expected classes on shlink-wrapper based on current pathname', async (pathname, isFlex) => {
|
||||||
const { container } = await setUp(pathname);
|
await setUp(pathname);
|
||||||
const shlinkWrapper = container.querySelector('.shlink-wrapper');
|
const shlinkWrapper = screen.getByTestId('shlink-wrapper');
|
||||||
|
|
||||||
expect(shlinkWrapper).toHaveAttribute('class', expectedClasses);
|
if (isFlex) {
|
||||||
|
expect(shlinkWrapper).toHaveClass('tw:flex');
|
||||||
|
} else {
|
||||||
|
expect(shlinkWrapper).not.toHaveClass('tw:flex');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { checkAccessibility } from '../__helpers__/accessibility';
|
|||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
|
|
||||||
describe('<AppUpdateBanner />', () => {
|
describe('<AppUpdateBanner />', () => {
|
||||||
const toggle = vi.fn();
|
const onClose = vi.fn();
|
||||||
const forceUpdate = vi.fn();
|
const forceUpdate = vi.fn();
|
||||||
const setUp = async () => {
|
const setUp = async () => {
|
||||||
const result = await act(
|
const result = await act(
|
||||||
() => renderWithEvents(<AppUpdateBanner isOpen toggle={toggle} forceUpdate={forceUpdate} />),
|
() => renderWithEvents(<AppUpdateBanner isOpen onClose={onClose} forceUpdate={forceUpdate} />),
|
||||||
);
|
);
|
||||||
await waitFor(() => screen.getByRole('alert'));
|
await waitFor(() => screen.getByRole('alert'));
|
||||||
|
|
||||||
@@ -28,9 +28,9 @@ describe('<AppUpdateBanner />', () => {
|
|||||||
it('invokes toggle when alert is closed', async () => {
|
it('invokes toggle when alert is closed', async () => {
|
||||||
const { user } = await setUp();
|
const { user } = await setUp();
|
||||||
|
|
||||||
expect(toggle).not.toHaveBeenCalled();
|
expect(onClose).not.toHaveBeenCalled();
|
||||||
await user.click(screen.getByLabelText('Close'));
|
await user.click(screen.getByLabelText('Close'));
|
||||||
expect(toggle).toHaveBeenCalled();
|
expect(onClose).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('triggers the update when clicking the button', async () => {
|
it('triggers the update when clicking the button', async () => {
|
||||||
|
|||||||
@@ -51,14 +51,12 @@ describe('<MainHeader />', () => {
|
|||||||
const toggle = screen.getByLabelText('Toggle navigation');
|
const toggle = screen.getByLabelText('Toggle navigation');
|
||||||
const icon = toggle.firstChild;
|
const icon = toggle.firstChild;
|
||||||
|
|
||||||
expect(icon).toHaveAttribute('class', expect.stringMatching(/main-header__toggle-icon$/));
|
expect(icon).not.toHaveClass('tw:rotate-180');
|
||||||
await user.click(toggle);
|
await user.click(toggle);
|
||||||
expect(icon).toHaveAttribute(
|
|
||||||
'class',
|
expect(icon).toHaveClass('tw:rotate-180');
|
||||||
expect.stringMatching(/main-header__toggle-icon main-header__toggle-icon--opened$/),
|
|
||||||
);
|
|
||||||
await user.click(toggle);
|
await user.click(toggle);
|
||||||
expect(icon).toHaveAttribute('class', expect.stringMatching(/main-header__toggle-icon$/));
|
expect(icon).not.toHaveClass('tw:rotate-180');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens Collapse when clicking toggle', async () => {
|
it('opens Collapse when clicking toggle', async () => {
|
||||||
|
|||||||
@@ -30,6 +30,5 @@ describe('<NotFound />', () => {
|
|||||||
|
|
||||||
expect(link).toHaveAttribute('href', expectedLink);
|
expect(link).toHaveAttribute('href', expectedLink);
|
||||||
expect(link).toHaveTextContent(expectedText);
|
expect(link).toHaveTextContent(expectedText);
|
||||||
expect(link).toHaveAttribute('class', 'btn btn-outline-primary btn-lg');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,11 +16,16 @@ describe('<ShlinkVersionsContainer />', () => {
|
|||||||
])('passes a11y checks', (selectedServer) => checkAccessibility(setUp(selectedServer)));
|
])('passes a11y checks', (selectedServer) => checkAccessibility(setUp(selectedServer)));
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[null, 'text-center'],
|
[null, false],
|
||||||
[fromPartial<SelectedServer>({}), 'text-center'],
|
[fromPartial<SelectedServer>({}), false],
|
||||||
[fromPartial<ReachableServer>({ version: '1.0.0' }), 'text-center shlink-versions-container--with-sidebar'],
|
[fromPartial<ReachableServer>({ version: '1.0.0' }), true],
|
||||||
])('renders proper col classes based on sidebar status', (selectedServer, expectedClasses) => {
|
])('renders proper col classes based on sidebar status', (selectedServer, shouldAddMargin) => {
|
||||||
const { container } = setUp(selectedServer);
|
const { container } = setUp(selectedServer);
|
||||||
expect(container.firstChild).toHaveAttribute('class', `${expectedClasses}`);
|
|
||||||
|
if (shouldAddMargin) {
|
||||||
|
expect(container.firstChild).toHaveClass('tw:md:ml-(--aside-menu-width)');
|
||||||
|
} else {
|
||||||
|
expect(container.firstChild).not.toHaveClass('tw:md:ml-(--aside-menu-width)');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,26 +1,35 @@
|
|||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import { Router } from 'react-router';
|
||||||
import { DeleteServerButtonFactory } from '../../src/servers/DeleteServerButton';
|
import { DeleteServerButtonFactory } from '../../src/servers/DeleteServerButton';
|
||||||
import type { DeleteServerModalProps } from '../../src/servers/DeleteServerModal';
|
import type { DeleteServerModalProps } from '../../src/servers/DeleteServerModal';
|
||||||
|
import { DeleteServerModal } from '../../src/servers/DeleteServerModal';
|
||||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
|
|
||||||
describe('<DeleteServerButton />', () => {
|
describe('<DeleteServerButton />', () => {
|
||||||
const DeleteServerButton = DeleteServerButtonFactory(fromPartial({
|
const DeleteServerButton = DeleteServerButtonFactory(fromPartial({
|
||||||
DeleteServerModal: ({ isOpen }: DeleteServerModalProps) => <>DeleteServerModal {isOpen ? '[Open]' : '[Closed]'}</>,
|
DeleteServerModal: (props: DeleteServerModalProps) => <DeleteServerModal {...props} deleteServer={vi.fn()} />,
|
||||||
}));
|
}));
|
||||||
const setUp = (children?: ReactNode) => renderWithEvents(
|
const setUp = (children: ReactNode = 'Remove this server') => {
|
||||||
<DeleteServerButton server={fromPartial({})} textClassName="button">{children}</DeleteServerButton>,
|
const history = createMemoryHistory({ initialEntries: ['/foo'] });
|
||||||
|
const result = renderWithEvents(
|
||||||
|
<Router location={history.location} navigator={history}>
|
||||||
|
<DeleteServerButton server={fromPartial({})}>{children}</DeleteServerButton>
|
||||||
|
</Router>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return { history, ...result };
|
||||||
|
};
|
||||||
|
|
||||||
it('passes a11y checks', () => checkAccessibility(setUp('Delete me')));
|
it('passes a11y checks', () => checkAccessibility(setUp('Delete me')));
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['Foo bar'],
|
['Foo bar'],
|
||||||
['baz'],
|
['baz'],
|
||||||
['something'],
|
['something'],
|
||||||
[undefined],
|
|
||||||
])('renders expected content', (children) => {
|
])('renders expected content', (children) => {
|
||||||
const { container } = setUp(children);
|
const { container } = setUp(children);
|
||||||
expect(container.firstChild).toBeTruthy();
|
expect(container.firstChild).toBeTruthy();
|
||||||
@@ -28,14 +37,21 @@ describe('<DeleteServerButton />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('displays modal when button is clicked', async () => {
|
it('displays modal when button is clicked', async () => {
|
||||||
const { user, container } = setUp();
|
const { user } = setUp();
|
||||||
|
|
||||||
expect(screen.getByText(/DeleteServerModal/)).toHaveTextContent(/Closed/);
|
expect(screen.queryByText(/Are you sure you want to remove/)).not.toBeInTheDocument();
|
||||||
expect(screen.getByText(/DeleteServerModal/)).not.toHaveTextContent(/Open/);
|
await user.click(screen.getByText('Remove this server'));
|
||||||
if (container.firstElementChild) {
|
expect(screen.getByText(/Are you sure you want to remove/)).toBeInTheDocument();
|
||||||
await user.click(container.firstElementChild);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
await waitFor(() => expect(screen.getByText(/DeleteServerModal/)).toHaveTextContent(/Open/));
|
it('navigates to home when deletion is confirmed', async () => {
|
||||||
|
const { user, history } = setUp();
|
||||||
|
|
||||||
|
// Open modal
|
||||||
|
await user.click(screen.getByText('Remove this server'));
|
||||||
|
|
||||||
|
expect(history.location.pathname).toEqual('/foo');
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Delete' }));
|
||||||
|
expect(history.location.pathname).toEqual('/');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { act, screen, waitFor } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { createMemoryHistory } from 'history';
|
|
||||||
import { Router } from 'react-router';
|
|
||||||
import { DeleteServerModal } from '../../src/servers/DeleteServerModal';
|
import { DeleteServerModal } from '../../src/servers/DeleteServerModal';
|
||||||
import { checkAccessibility } from '../__helpers__/accessibility';
|
import { checkAccessibility } from '../__helpers__/accessibility';
|
||||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||||
@@ -10,10 +8,7 @@ import { TestModalWrapper } from '../__helpers__/TestModalWrapper';
|
|||||||
describe('<DeleteServerModal />', () => {
|
describe('<DeleteServerModal />', () => {
|
||||||
const deleteServerMock = vi.fn();
|
const deleteServerMock = vi.fn();
|
||||||
const serverName = 'the_server_name';
|
const serverName = 'the_server_name';
|
||||||
const setUp = async () => {
|
const setUp = () => renderWithEvents(
|
||||||
const history = createMemoryHistory({ initialEntries: ['/foo'] });
|
|
||||||
const result = await act(() => renderWithEvents(
|
|
||||||
<Router location={history.location} navigator={history}>
|
|
||||||
<TestModalWrapper
|
<TestModalWrapper
|
||||||
renderModal={(args) => (
|
renderModal={(args) => (
|
||||||
<DeleteServerModal
|
<DeleteServerModal
|
||||||
@@ -22,50 +17,42 @@ describe('<DeleteServerModal />', () => {
|
|||||||
deleteServer={deleteServerMock}
|
deleteServer={deleteServerMock}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>,
|
||||||
</Router>,
|
);
|
||||||
));
|
|
||||||
|
|
||||||
return { history, ...result };
|
|
||||||
};
|
|
||||||
|
|
||||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||||
|
|
||||||
it('renders a modal window', async () => {
|
it('renders a modal window', () => {
|
||||||
await setUp();
|
setUp();
|
||||||
|
|
||||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('heading')).toHaveTextContent('Remove server');
|
expect(screen.getByRole('heading')).toHaveTextContent('Remove server');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays the name of the server as part of the content', async () => {
|
it('displays the name of the server as part of the content', () => {
|
||||||
await setUp();
|
setUp();
|
||||||
|
|
||||||
expect(screen.getByText(/^Are you sure you want to remove/)).toBeInTheDocument();
|
expect(screen.getByText(/^Are you sure you want to remove/)).toBeInTheDocument();
|
||||||
expect(screen.getByText(serverName)).toBeInTheDocument();
|
expect(screen.getByText(serverName)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.only.each([
|
||||||
[() => screen.getByRole('button', { name: 'Cancel' })],
|
[() => screen.getByRole('button', { name: 'Cancel' })],
|
||||||
[() => screen.getByLabelText('Close')],
|
[() => screen.getByLabelText('Close dialog')],
|
||||||
])('toggles when clicking cancel button', async (getButton) => {
|
])('closes dialog when clicking cancel button', async (getButton) => {
|
||||||
const { user, history } = await setUp();
|
const { user } = setUp();
|
||||||
|
|
||||||
expect(history.location.pathname).toEqual('/foo');
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||||
await user.click(getButton());
|
await user.click(getButton());
|
||||||
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
expect(deleteServerMock).not.toHaveBeenCalled();
|
expect(deleteServerMock).not.toHaveBeenCalled();
|
||||||
expect(history.location.pathname).toEqual('/foo'); // No navigation happens, keeping initial pathname
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes server when clicking accept button', async () => {
|
it('deletes server when clicking accept button', async () => {
|
||||||
const { user, history } = await setUp();
|
const { user } = setUp();
|
||||||
|
|
||||||
expect(deleteServerMock).not.toHaveBeenCalled();
|
expect(deleteServerMock).not.toHaveBeenCalled();
|
||||||
expect(history.location.pathname).toEqual('/foo');
|
|
||||||
await user.click(screen.getByRole('button', { name: 'Delete' }));
|
await user.click(screen.getByRole('button', { name: 'Delete' }));
|
||||||
|
expect(deleteServerMock).toHaveBeenCalledOnce();
|
||||||
await waitFor(() => expect(deleteServerMock).toHaveBeenCalledTimes(1));
|
|
||||||
await waitFor(() => expect(history.location.pathname).toEqual('/'));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Table } from '@shlinkio/shlink-frontend-kit/tailwind';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { fromPartial } from '@total-typescript/shoehorn';
|
import { fromPartial } from '@total-typescript/shoehorn';
|
||||||
import { MemoryRouter } from 'react-router';
|
import { MemoryRouter } from 'react-router';
|
||||||
@@ -17,11 +18,9 @@ describe('<ManageServersRow />', () => {
|
|||||||
};
|
};
|
||||||
const setUp = (hasAutoConnect = false, autoConnect = false) => render(
|
const setUp = (hasAutoConnect = false, autoConnect = false) => render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<table>
|
<Table header={<Table.Row />}>
|
||||||
<tbody>
|
|
||||||
<ManageServersRow server={{ ...server, autoConnect }} hasAutoConnect={hasAutoConnect} />
|
<ManageServersRow server={{ ...server, autoConnect }} hasAutoConnect={hasAutoConnect} />
|
||||||
</tbody>
|
</Table>
|
||||||
</table>
|
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -32,11 +31,7 @@ describe('<ManageServersRow />', () => {
|
|||||||
[false, 3],
|
[false, 3],
|
||||||
])('renders expected amount of columns', (hasAutoConnect, expectedCols) => {
|
])('renders expected amount of columns', (hasAutoConnect, expectedCols) => {
|
||||||
setUp(hasAutoConnect);
|
setUp(hasAutoConnect);
|
||||||
|
expect(screen.getAllByRole('cell')).toHaveLength(expectedCols);
|
||||||
const td = screen.getAllByRole('cell');
|
|
||||||
const th = screen.getAllByRole('columnheader');
|
|
||||||
|
|
||||||
expect(td.length + th.length).toEqual(expectedCols);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a dropdown', () => {
|
it('renders a dropdown', () => {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { renderWithEvents } from '../__helpers__/setUpTest';
|
|||||||
|
|
||||||
describe('<ManageServersRowDropdown />', () => {
|
describe('<ManageServersRowDropdown />', () => {
|
||||||
const ManageServersRowDropdown = ManageServersRowDropdownFactory(fromPartial({
|
const ManageServersRowDropdown = ManageServersRowDropdownFactory(fromPartial({
|
||||||
DeleteServerModal: ({ isOpen }: { isOpen: boolean }) => (
|
DeleteServerModal: ({ open }: { open: boolean }) => (
|
||||||
<span>DeleteServerModal {isOpen ? '[OPEN]' : '[CLOSED]'}</span>
|
<span>DeleteServerModal {open ? '[OPEN]' : '[CLOSED]'}</span>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
const setAutoConnect = vi.fn();
|
const setAutoConnect = vi.fn();
|
||||||
|
|||||||
@@ -10,30 +10,18 @@ describe('<ServersListGroup />', () => {
|
|||||||
fromPartial({ name: 'foo', id: '123' }),
|
fromPartial({ name: 'foo', id: '123' }),
|
||||||
fromPartial({ name: 'bar', id: '456' }),
|
fromPartial({ name: 'bar', id: '456' }),
|
||||||
];
|
];
|
||||||
const setUp = (params: { servers?: ServerWithId[]; withChildren?: boolean; embedded?: boolean } = {}) => {
|
const setUp = (params: { servers?: ServerWithId[]; borderless?: boolean } = {}) => {
|
||||||
const { servers = [], withChildren = true, embedded } = params;
|
const { servers = [], borderless } = params;
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ServersListGroup servers={servers} embedded={embedded}>
|
<ServersListGroup servers={servers} borderless={borderless} />
|
||||||
{withChildren ? 'The list of servers' : undefined}
|
|
||||||
</ServersListGroup>
|
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||||
|
|
||||||
it('renders title', () => {
|
|
||||||
setUp({});
|
|
||||||
expect(screen.getByTestId('title')).toHaveTextContent('The list of servers');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not render title when children is not provided', () => {
|
|
||||||
setUp({ withChildren: false });
|
|
||||||
expect(screen.queryByTestId('title')).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[servers],
|
[servers],
|
||||||
[[]],
|
[[]],
|
||||||
@@ -45,11 +33,17 @@ describe('<ServersListGroup />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[true, 'servers-list__list-group servers-list__list-group--embedded'],
|
[true],
|
||||||
[false, 'servers-list__list-group'],
|
[false],
|
||||||
[undefined, 'servers-list__list-group'],
|
[undefined],
|
||||||
])('renders proper classes for embedded', (embedded, expectedClasses) => {
|
])('renders proper classes for embedded', (borderless) => {
|
||||||
setUp({ servers, embedded });
|
setUp({ servers, borderless });
|
||||||
expect(screen.getByTestId('list')).toHaveAttribute('class', `${expectedClasses} list-group`);
|
const list = screen.getByTestId('list');
|
||||||
|
|
||||||
|
if (!borderless) {
|
||||||
|
expect(list).toHaveClass('tw:border-y');
|
||||||
|
} else {
|
||||||
|
expect(list).not.toHaveClass('tw:border-y');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,67 +2,27 @@
|
|||||||
|
|
||||||
exports[`<DeleteServerButton /> > renders expected content 1`] = `
|
exports[`<DeleteServerButton /> > renders expected content 1`] = `
|
||||||
<button
|
<button
|
||||||
class="p-0 bg-transparent border-0"
|
class="tw:text-danger tw:hover:underline"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
class="button"
|
|
||||||
>
|
|
||||||
Foo bar
|
Foo bar
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<DeleteServerButton /> > renders expected content 2`] = `
|
exports[`<DeleteServerButton /> > renders expected content 2`] = `
|
||||||
<button
|
<button
|
||||||
class="p-0 bg-transparent border-0"
|
class="tw:text-danger tw:hover:underline"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
class="button"
|
|
||||||
>
|
|
||||||
baz
|
baz
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<DeleteServerButton /> > renders expected content 3`] = `
|
exports[`<DeleteServerButton /> > renders expected content 3`] = `
|
||||||
<button
|
<button
|
||||||
class="p-0 bg-transparent border-0"
|
class="tw:text-danger tw:hover:underline"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span
|
|
||||||
class="button"
|
|
||||||
>
|
|
||||||
something
|
something
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<DeleteServerButton /> > renders expected content 4`] = `
|
|
||||||
<button
|
|
||||||
class="p-0 bg-transparent border-0"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
class="svg-inline--fa fa-circle-minus fa-fw "
|
|
||||||
data-icon="circle-minus"
|
|
||||||
data-prefix="fas"
|
|
||||||
focusable="false"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM184 232l144 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-144 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span
|
|
||||||
class="button"
|
|
||||||
>
|
|
||||||
Remove this server
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -2,18 +2,29 @@
|
|||||||
|
|
||||||
exports[`<ManageServersRow /> > renders auto-connect icon only if server is autoConnect 1`] = `
|
exports[`<ManageServersRow /> > renders auto-connect icon only if server is autoConnect 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<table>
|
<table
|
||||||
<tbody>
|
class="tw:w-full"
|
||||||
|
>
|
||||||
|
<thead
|
||||||
|
class="tw:hidden tw:lg:table-header-group"
|
||||||
|
>
|
||||||
<tr
|
<tr
|
||||||
class="responsive-table__row"
|
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"
|
||||||
|
/>
|
||||||
|
</thead>
|
||||||
|
<tbody
|
||||||
|
class="tw:lg:table-row-group tw:flex tw:flex-col tw: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"
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
class="responsive-table__cell"
|
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"
|
||||||
data-th="Auto-connect"
|
data-column="Auto-connect"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="svg-inline--fa fa-check text-primary"
|
class="svg-inline--fa fa-check tw:text-brand"
|
||||||
data-icon="check"
|
data-icon="check"
|
||||||
data-prefix="fas"
|
data-prefix="fas"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
@@ -28,9 +39,9 @@ exports[`<ManageServersRow /> > renders auto-connect icon only if server is auto
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</td>
|
</td>
|
||||||
<th
|
<td
|
||||||
class="responsive-table__cell"
|
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"
|
||||||
data-th="Name"
|
data-column="Name"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
@@ -38,15 +49,15 @@ exports[`<ManageServersRow /> > renders auto-connect icon only if server is auto
|
|||||||
>
|
>
|
||||||
My server
|
My server
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="responsive-table__cell"
|
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"
|
||||||
data-th="Base URL"
|
data-column="Base URL"
|
||||||
>
|
>
|
||||||
https://example.com
|
https://example.com
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="responsive-table__cell text-end"
|
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"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
ManageServersRowDropdown
|
ManageServersRowDropdown
|
||||||
@@ -60,18 +71,29 @@ exports[`<ManageServersRow /> > renders auto-connect icon only if server is auto
|
|||||||
|
|
||||||
exports[`<ManageServersRow /> > renders auto-connect icon only if server is autoConnect 2`] = `
|
exports[`<ManageServersRow /> > renders auto-connect icon only if server is autoConnect 2`] = `
|
||||||
<div>
|
<div>
|
||||||
<table>
|
<table
|
||||||
<tbody>
|
class="tw:w-full"
|
||||||
|
>
|
||||||
|
<thead
|
||||||
|
class="tw:hidden tw:lg:table-header-group"
|
||||||
|
>
|
||||||
<tr
|
<tr
|
||||||
class="responsive-table__row"
|
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"
|
||||||
|
/>
|
||||||
|
</thead>
|
||||||
|
<tbody
|
||||||
|
class="tw:lg:table-row-group tw:flex tw:flex-col tw: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"
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
class="responsive-table__cell"
|
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"
|
||||||
data-th="Auto-connect"
|
data-column="Auto-connect"
|
||||||
/>
|
/>
|
||||||
<th
|
<td
|
||||||
class="responsive-table__cell"
|
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"
|
||||||
data-th="Name"
|
data-column="Name"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
data-discover="true"
|
data-discover="true"
|
||||||
@@ -79,15 +101,15 @@ exports[`<ManageServersRow /> > renders auto-connect icon only if server is auto
|
|||||||
>
|
>
|
||||||
My server
|
My server
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="responsive-table__cell"
|
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"
|
||||||
data-th="Base URL"
|
data-column="Base URL"
|
||||||
>
|
>
|
||||||
https://example.com
|
https://example.com
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="responsive-table__cell text-end"
|
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"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
ManageServersRowDropdown
|
ManageServersRowDropdown
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ exports[`<ManageServersRowDropdown /> > renders expected size and icon 1`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="dropdown-item--danger dropdown-item"
|
class="tw:text-danger dropdown-item"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -259,7 +259,7 @@ exports[`<ManageServersRowDropdown /> > renders expected size and icon 2`] = `
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="dropdown-item--danger dropdown-item"
|
class="tw:text-danger dropdown-item"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import { checkAccessibility } from '../../__helpers__/accessibility';
|
|||||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||||
|
|
||||||
describe('<DuplicatedServersModal />', () => {
|
describe('<DuplicatedServersModal />', () => {
|
||||||
const onDiscard = vi.fn();
|
const onClose = vi.fn();
|
||||||
const onSave = vi.fn();
|
const onConfirm = vi.fn();
|
||||||
const setUp = (duplicatedServers: ServerData[] = []) => act(() => renderWithEvents(
|
const setUp = (duplicatedServers: ServerData[] = []) => act(() => renderWithEvents(
|
||||||
<DuplicatedServersModal isOpen duplicatedServers={duplicatedServers} onDiscard={onDiscard} onSave={onSave} />,
|
<DuplicatedServersModal open duplicatedServers={duplicatedServers} onClose={onClose} onConfirm={onConfirm} />,
|
||||||
));
|
));
|
||||||
const mockServer = (data: Partial<ServerData> = {}) => fromPartial<ServerData>(data);
|
const mockServer = (data: Partial<ServerData> = {}) => fromPartial<ServerData>(data);
|
||||||
|
|
||||||
@@ -32,8 +32,9 @@ describe('<DuplicatedServersModal />', () => {
|
|||||||
{
|
{
|
||||||
header: 'Duplicated server',
|
header: 'Duplicated server',
|
||||||
firstParagraph: 'There is already a server with:',
|
firstParagraph: 'There is already a server with:',
|
||||||
lastParagraph: 'Do you want to save this server anyway?',
|
lastParagraph: 'Do you want to save this server?',
|
||||||
discardBtn: 'Discard',
|
discardBtn: 'Discard',
|
||||||
|
confirmButton: 'Save duplicate',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -41,8 +42,9 @@ describe('<DuplicatedServersModal />', () => {
|
|||||||
{
|
{
|
||||||
header: 'Duplicated servers',
|
header: 'Duplicated servers',
|
||||||
firstParagraph: 'The next servers already exist:',
|
firstParagraph: 'The next servers already exist:',
|
||||||
lastParagraph: 'Do you want to ignore duplicated servers?',
|
lastParagraph: 'Do you want to save duplicated servers?',
|
||||||
discardBtn: 'Ignore duplicates',
|
discardBtn: 'Ignore duplicates',
|
||||||
|
confirmButton: 'Save duplicates',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
])('renders expected texts based on amount of servers', async (duplicatedServers, assertions) => {
|
])('renders expected texts based on amount of servers', async (duplicatedServers, assertions) => {
|
||||||
@@ -52,6 +54,7 @@ describe('<DuplicatedServersModal />', () => {
|
|||||||
expect(screen.getByText(assertions.firstParagraph)).toBeInTheDocument();
|
expect(screen.getByText(assertions.firstParagraph)).toBeInTheDocument();
|
||||||
expect(screen.getByText(assertions.lastParagraph)).toBeInTheDocument();
|
expect(screen.getByText(assertions.lastParagraph)).toBeInTheDocument();
|
||||||
expect(screen.getByRole('button', { name: assertions.discardBtn })).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: assertions.discardBtn })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: assertions.confirmButton })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
@@ -80,19 +83,19 @@ describe('<DuplicatedServersModal />', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('invokes onDiscard when appropriate button is clicked', async () => {
|
it('invokes onClose when appropriate button is clicked', async () => {
|
||||||
const { user } = await setUp();
|
const { user } = await setUp();
|
||||||
|
|
||||||
expect(onDiscard).not.toHaveBeenCalled();
|
expect(onClose).not.toHaveBeenCalled();
|
||||||
await user.click(screen.getByRole('button', { name: 'Discard' }));
|
await user.click(screen.getByRole('button', { name: 'Discard' }));
|
||||||
expect(onDiscard).toHaveBeenCalled();
|
expect(onClose).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('invokes onSave when appropriate button is clicked', async () => {
|
it('invokes onConfirm when appropriate button is clicked', async () => {
|
||||||
const { user } = await setUp();
|
const { user } = await setUp();
|
||||||
|
|
||||||
expect(onSave).not.toHaveBeenCalled();
|
expect(onConfirm).not.toHaveBeenCalled();
|
||||||
await user.click(screen.getByRole('button', { name: 'Save anyway' }));
|
await user.click(screen.getByRole('button', { name: 'Save duplicate' }));
|
||||||
expect(onSave).toHaveBeenCalled();
|
expect(onConfirm).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ describe('<ImportServersBtn />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{ btnName: 'Save anyway',savesDuplicatedServers: true },
|
{ btnName: 'Save duplicate', savesDuplicatedServers: true },
|
||||||
{ btnName: 'Discard', savesDuplicatedServers: false },
|
{ btnName: 'Discard', savesDuplicatedServers: false },
|
||||||
])('creates expected servers depending on selected option in modal', async ({ btnName, savesDuplicatedServers }) => {
|
])('creates duplicated servers depending on selected option in modal', async ({ btnName, savesDuplicatedServers }) => {
|
||||||
const existingServerData: ServerData = {
|
const existingServerData: ServerData = {
|
||||||
name: 'existingServer',
|
name: 'existingServer',
|
||||||
url: 'http://s.test/existingUrl',
|
url: 'http://s.test/existingUrl',
|
||||||
@@ -84,14 +84,20 @@ describe('<ImportServersBtn />', () => {
|
|||||||
|
|
||||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
await user.upload(screen.getByTestId('csv-file-input'), csvFile);
|
await user.upload(screen.getByTestId('csv-file-input'), csvFile);
|
||||||
|
|
||||||
|
// Once the file is uploaded, non-duplicated servers are immediately created
|
||||||
|
expect(createServersMock).toHaveBeenCalledExactlyOnceWith([expect.objectContaining(newServer)]);
|
||||||
|
|
||||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||||
await user.click(screen.getByRole('button', { name: btnName }));
|
await user.click(screen.getByRole('button', { name: btnName }));
|
||||||
|
|
||||||
expect(createServersMock).toHaveBeenCalledWith(
|
// If duplicated servers are saved, there's one extra call
|
||||||
savesDuplicatedServers
|
if (savesDuplicatedServers) {
|
||||||
? [expect.objectContaining(existingServerData), expect.objectContaining(newServer)]
|
expect(createServersMock).toHaveBeenLastCalledWith([expect.objectContaining(existingServerData)]);
|
||||||
: [expect.objectContaining(newServer)],
|
}
|
||||||
);
|
|
||||||
expect(onImportMock).toHaveBeenCalledTimes(1);
|
// On import is called only once, no matter what
|
||||||
|
expect(onImportMock).toHaveBeenCalledOnce();
|
||||||
|
expect(createServersMock).toHaveBeenCalledTimes(savesDuplicatedServers ? 2 : 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ describe('<ServerForm />', () => {
|
|||||||
|
|
||||||
it('passes a11y checks', () => checkAccessibility(setUp()));
|
it('passes a11y checks', () => checkAccessibility(setUp()));
|
||||||
|
|
||||||
it('renders components', () => {
|
it('renders inputs', () => {
|
||||||
setUp();
|
setUp();
|
||||||
|
|
||||||
expect(screen.getAllByRole('textbox')).toHaveLength(3);
|
expect(screen.getByLabelText(/^Name/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText(/^URL/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText(/^API key/)).toBeInTheDocument();
|
||||||
expect(screen.getByText('Something')).toBeInTheDocument();
|
expect(screen.getByText('Something')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
@@ -11,7 +12,7 @@ const homepage = pack.homepage?.trim();
|
|||||||
|
|
||||||
/* eslint-disable-next-line no-restricted-exports */
|
/* eslint-disable-next-line no-restricted-exports */
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), VitePWA({
|
plugins: [react(), tailwindcss(), VitePWA({
|
||||||
mode: process.env.NODE_ENV === 'development' ? 'development' : 'production',
|
mode: process.env.NODE_ENV === 'development' ? 'development' : 'production',
|
||||||
strategies: 'injectManifest',
|
strategies: 'injectManifest',
|
||||||
srcDir: './src',
|
srcDir: './src',
|
||||||
@@ -25,6 +26,10 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
|
watch: {
|
||||||
|
// Do not watch test files or generated files, avoiding the dev server to constantly reload when not needed
|
||||||
|
ignored: ['**/.idea/**', '**/.git/**', '**/build/**', '**/coverage/**', '**/test/**'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
base: !homepage ? undefined : homepage, // Not using just homepage because empty string should be discarded
|
base: !homepage ? undefined : homepage, // Not using just homepage because empty string should be discarded
|
||||||
|
|
||||||
@@ -55,6 +60,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Silent warnings triggered by reactstrap components, as it's getting removed
|
||||||
|
onConsoleLog: (log) => !log.includes('`transition.timeout` is marked as required'),
|
||||||
|
|
||||||
// Workaround for bug in react-router (or vitest module resolution) which causes different react-router versions to
|
// Workaround for bug in react-router (or vitest module resolution) which causes different react-router versions to
|
||||||
// be resolved for the main package and dependencies who have a peer dependency in react-router.
|
// be resolved for the main package and dependencies who have a peer dependency in react-router.
|
||||||
// This ensures always the same version is resolved.
|
// This ensures always the same version is resolved.
|
||||||
|
|||||||
Reference in New Issue
Block a user