From 2c79b64042a8911498ac856ba631ed9a9def290d Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Tue, 26 May 2026 19:39:47 -0400 Subject: [PATCH 01/15] feat: add file citation download --- CHANGELOG.md | 1 + package-lock.json | 2544 ++--------------- package.json | 2 +- public/locales/en/shared.json | 3 + public/locales/es/shared.json | 3 + src/files/domain/models/FileCitation.ts | 12 + .../domain/repositories/FileRepository.ts | 5 + .../useCases/getFileCitationByFormat.ts | 10 + .../FileJSDataverseRepository.ts | 50 + src/sections/file/File.tsx | 7 +- .../file/file-citation/FileCitation.tsx | 18 +- .../CitationDownloadButton.tsx | 29 +- .../FileCitationDownloadButton.tsx | 58 + .../citation-download/useDownloadCitation.tsx | 7 +- .../useDownloadFileCitation.tsx | 45 + src/stories/file/FileMockRepository.ts | 15 + .../file-citation/FileCitation.stories.tsx | 9 + tests/component/sections/file/File.spec.tsx | 1 + .../file/file-citation/FileCitation.spec.tsx | 22 +- .../citation/CitationDownloadButton.spec.tsx | 3 +- .../FileCitationDownloadButton.spec.tsx | 63 + 21 files changed, 567 insertions(+), 2340 deletions(-) create mode 100644 src/files/domain/models/FileCitation.ts create mode 100644 src/files/domain/useCases/getFileCitationByFormat.ts create mode 100644 src/sections/shared/citation/citation-download/FileCitationDownloadButton.tsx create mode 100644 src/sections/shared/citation/citation-download/useDownloadFileCitation.tsx create mode 100644 tests/component/sections/shared/citation/FileCitationDownloadButton.spec.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a05b41c..dab50f38c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added ### Changed +- File pages now include a "Cite Data File" dropdown for downloading file citations in EndNote XML, RIS, and BibTeX formats. ### Fixed diff --git a/package-lock.json b/package-lock.json index c5b242488..1a04509ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.1.0-alpha.4", + "@iqss/dataverse-client-javascript": "v2.2.0-pr446.56b9609", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -230,93 +230,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "regexpu-core": "^6.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", - "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "debug": "^4.4.3", - "lodash.debounce": "^4.0.8", - "resolve": "^1.22.11" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -326,21 +239,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", @@ -371,20 +269,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-plugin-utils": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", @@ -394,59 +278,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -474,22 +305,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", - "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helpers": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", @@ -518,109 +333,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", - "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", - "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -676,23 +388,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", - "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", @@ -700,922 +395,7 @@ "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", - "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", - "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-remap-async-to-generator": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", - "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/template": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", - "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", - "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", - "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", - "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", - "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", - "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", - "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", - "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", - "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1624,48 +404,37 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", - "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", - "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-reserved-words": { + "node_modules/@babel/plugin-syntax-jsx": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", - "dev": true, + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -1676,119 +445,92 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", - "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { "node": ">=6.9.0" @@ -1797,16 +539,14 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { "node": ">=6.9.0" @@ -1815,102 +555,30 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", - "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-env": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz", - "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==", + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/compat-data": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.28.6", - "@babel/plugin-syntax-import-attributes": "^7.28.6", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.29.0", - "@babel/plugin-transform-async-to-generator": "^7.28.6", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.6", - "@babel/plugin-transform-class-properties": "^7.28.6", - "@babel/plugin-transform-class-static-block": "^7.28.6", - "@babel/plugin-transform-classes": "^7.28.6", - "@babel/plugin-transform-computed-properties": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-dotall-regex": "^7.28.6", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.6", - "@babel/plugin-transform-exponentiation-operator": "^7.28.6", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.28.6", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@babel/plugin-transform-modules-systemjs": "^7.29.0", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", - "@babel/plugin-transform-numeric-separator": "^7.28.6", - "@babel/plugin-transform-object-rest-spread": "^7.28.6", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.28.6", - "@babel/plugin-transform-optional-chaining": "^7.28.6", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.28.6", - "@babel/plugin-transform-private-property-in-object": "^7.28.6", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.29.0", - "@babel/plugin-transform-regexp-modifiers": "^7.28.6", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.28.6", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.28.6", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.15", - "babel-plugin-polyfill-corejs3": "^0.14.0", - "babel-plugin-polyfill-regenerator": "^0.6.6", - "core-js-compat": "^3.48.0", - "semver": "^6.3.1" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1919,20 +587,20 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/runtime": { @@ -3285,9 +1953,9 @@ } }, "node_modules/@iqss/dataverse-client-javascript": { - "version": "2.1.0-alpha.4", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.1.0-alpha.4/0a0dc68d4d99581d7ec017e58dbce3407f99f5d9", - "integrity": "sha512-UwHnFSYuvhxpc/JG2cFr5+bwERZXqGfNBTMSUQwtJaj6vfHO7anJAAxAx8g/AB8b4JtR9rljnYjAbsGJXicfBw==", + "version": "2.2.0-pr446.56b9609", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.2.0-pr446.56b9609/24962a5af33aeda8a69c6e5add63fa30bf962055", + "integrity": "sha512-LoO2giTz9eUxclH6q1FYXUEN717GYchUQGEeEsM9AMHKLF9nzGWNz4OcI0dj/0N+u+4LrB2xYpckT1jXBCVn2Q==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", @@ -4792,18 +3460,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "devOptional": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -7019,6 +5675,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7032,6 +5689,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7045,6 +5703,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7058,6 +5717,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7071,6 +5731,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7084,6 +5745,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7097,6 +5759,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7110,6 +5773,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7123,6 +5787,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7136,6 +5801,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7149,6 +5815,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7162,6 +5829,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7175,6 +5843,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7188,6 +5857,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7201,6 +5871,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7214,6 +5885,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7227,6 +5899,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7240,6 +5913,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7253,6 +5927,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7266,6 +5941,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7279,6 +5955,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7292,6 +5969,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -8794,6 +7472,7 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", @@ -9574,34 +8253,11 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, "node_modules/@types/graceful-fs": { @@ -10408,198 +9064,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true - }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -10680,7 +9144,7 @@ "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -10700,20 +9164,6 @@ "acorn-walk": "^8.0.2" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -10761,6 +9211,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", @@ -10818,20 +9269,6 @@ } } }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -10937,6 +9374,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, "license": "MIT", "dependencies": { "default-require-extensions": "^3.0.0" @@ -10977,6 +9415,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -11349,154 +9788,6 @@ "@babel/core": "^7.8.0" } }, - "node_modules/babel-loader": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", - "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "find-cache-dir": "^4.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5" - } - }, - "node_modules/babel-loader/node_modules/find-cache-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", - "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "common-path-prefix": "^3.0.0", - "pkg-dir": "^7.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-loader/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-loader/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-loader/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-loader/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-loader/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/babel-loader/node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "find-up": "^6.3.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-loader/node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", @@ -11564,51 +9855,6 @@ "dev": true, "license": "MIT" }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", - "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.8", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", - "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8", - "core-js-compat": "^3.48.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", - "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/babel-plugin-styled-components": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", @@ -12004,7 +10250,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/byte-size": { @@ -12112,6 +10358,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, "license": "MIT", "dependencies": { "hasha": "^5.0.0", @@ -12127,6 +10374,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, "license": "MIT", "dependencies": { "semver": "^6.0.0" @@ -12142,6 +10390,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -12436,17 +10685,6 @@ } } }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.0" - } - }, "node_modules/ci-info": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", @@ -12479,6 +10717,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -12815,14 +11054,6 @@ "dev": true, "license": "ISC" }, - "node_modules/common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "dev": true, - "license": "ISC", - "peer": true - }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -12837,6 +11068,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, "license": "MIT" }, "node_modules/compare-func": { @@ -13065,21 +11297,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, - "node_modules/core-js-compat": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", - "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "browserslist": "^4.28.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -13202,6 +11419,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -13593,6 +11811,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13726,6 +11945,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, "license": "MIT", "dependencies": { "strip-bom": "^4.0.0" @@ -14248,12 +12468,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14264,6 +12486,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14283,21 +12506,6 @@ "once": "^1.4.0" } }, - "node_modules/enhanced-resolve": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", - "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -14471,14 +12679,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -14541,6 +12741,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, "license": "MIT" }, "node_modules/esbuild": { @@ -15226,17 +13427,6 @@ "dev": true, "license": "MIT" }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/execa": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", @@ -15570,6 +13760,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, "license": "MIT", "dependencies": { "commondir": "^1.0.1", @@ -15587,6 +13778,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, "license": "MIT", "dependencies": { "semver": "^6.0.0" @@ -15865,6 +14057,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, "funding": [ { "type": "github", @@ -16047,6 +14240,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -16517,14 +14711,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true - }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -16832,6 +15018,7 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, "license": "MIT", "dependencies": { "is-stream": "^2.0.0", @@ -16848,6 +15035,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=8" @@ -16950,6 +15138,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, "license": "MIT" }, "node_modules/html-parse-stringify": { @@ -17259,6 +15448,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -17684,6 +15874,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17936,6 +16127,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18010,6 +16202,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, "license": "MIT" }, "node_modules/is-unc-path": { @@ -18132,6 +16325,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/isstream": { @@ -18145,6 +16339,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=8" @@ -18154,6 +16349,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "append-transform": "^2.0.0" @@ -18206,6 +16402,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, "license": "ISC", "dependencies": { "archy": "^1.0.0", @@ -18223,6 +16420,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=8" @@ -18232,6 +16430,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" @@ -18244,6 +16443,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -18253,6 +16453,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", @@ -18267,6 +16468,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", @@ -18281,6 +16483,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", @@ -21932,21 +20135,6 @@ "node": ">=8" } }, - "node_modules/loader-runner": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", - "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -21969,18 +20157,11 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true, "license": "MIT" }, "node_modules/lodash.get": { @@ -22161,6 +20342,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" @@ -22176,6 +20358,7 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -23504,6 +21687,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -23819,6 +22003,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, "license": "MIT", "dependencies": { "process-on-spawn": "^1.0.0" @@ -24268,6 +22453,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/load-nyc-config": "^1.0.0", @@ -24309,6 +22495,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -24320,12 +22507,14 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, "license": "MIT" }, "node_modules/nyc/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -24339,6 +22528,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -24353,6 +22543,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -24373,6 +22564,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.7.5", @@ -24388,6 +22580,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -24400,6 +22593,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, "license": "MIT", "dependencies": { "semver": "^6.0.0" @@ -24415,6 +22609,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -24430,6 +22625,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -24442,6 +22638,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" @@ -24454,6 +22651,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -24463,6 +22661,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -24477,12 +22676,14 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, "license": "ISC" }, "node_modules/nyc/node_modules/yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, "license": "MIT", "dependencies": { "cliui": "^6.0.0", @@ -24505,6 +22706,7 @@ "version": "18.1.3", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, "license": "ISC", "dependencies": { "camelcase": "^5.0.0", @@ -24977,6 +23179,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, "license": "ISC", "dependencies": { "graceful-fs": "^4.1.15", @@ -25514,6 +23717,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -25633,6 +23837,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" @@ -25645,6 +23850,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -25658,6 +23864,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -25670,6 +23877,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -25685,6 +23893,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -25766,6 +23975,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -25987,6 +24197,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", + "dev": true, "license": "MIT", "dependencies": { "fromentries": "^1.2.0" @@ -26461,6 +24672,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -26562,6 +24774,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -27152,28 +25365,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -27207,51 +25398,11 @@ "url": "https://github.com/sponsors/mysticatea" } }, - "node_modules/regexpu-core": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", - "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.2", - "regjsgen": "^0.8.0", - "regjsparser": "^0.13.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.2.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/regjsparser": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", - "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "jsesc": "~3.1.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, "node_modules/release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, "license": "ISC", "dependencies": { "es6-error": "^4.0.1" @@ -27358,6 +25509,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -27376,6 +25528,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, "license": "ISC" }, "node_modules/requireindex": { @@ -27534,6 +25687,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -27550,6 +25704,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -27570,6 +25725,7 @@ "version": "4.52.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz", "integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -27748,7 +25904,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/sass": { @@ -27785,49 +25941,10 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } + "loose-envify": "^1.1.0" } }, "node_modules/semver": { @@ -27843,6 +25960,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, "license": "ISC" }, "node_modules/set-function-length": { @@ -27902,6 +26020,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -27914,6 +26033,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -28008,6 +26128,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, "license": "ISC" }, "node_modules/sigstore": { @@ -28157,6 +26278,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -28203,6 +26325,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^2.0.0", @@ -28220,6 +26343,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -28233,6 +26357,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -28242,6 +26367,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, "license": "MIT", "dependencies": { "semver": "^6.0.0" @@ -28483,6 +26609,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -28600,6 +26727,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -28626,6 +26754,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -29381,21 +27510,6 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -29511,114 +27625,6 @@ "node": ">=4" } }, - "node_modules/terser": { - "version": "5.46.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz", - "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==", - "devOptional": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.5.0.tgz", - "integrity": "sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/terser-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "devOptional": true, - "license": "MIT", - "peer": true - }, - "node_modules/terser/node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "devOptional": true, - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -30328,6 +28334,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, "license": "MIT", "dependencies": { "is-typedarray": "^1.0.0" @@ -30416,54 +28423,6 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", - "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", - "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/unified": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", @@ -31034,6 +28993,7 @@ "version": "5.4.20", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", @@ -31209,6 +29169,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31225,6 +29186,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31241,6 +29203,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31257,6 +29220,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31273,6 +29237,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31289,6 +29254,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31305,6 +29271,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31321,6 +29288,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31337,6 +29305,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31353,6 +29322,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31369,6 +29339,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31385,6 +29356,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31401,6 +29373,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31417,6 +29390,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31433,6 +29407,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31449,6 +29424,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31465,6 +29441,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31481,6 +29458,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31497,6 +29475,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31513,6 +29492,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31529,6 +29509,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31545,6 +29526,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31561,6 +29543,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -31574,6 +29557,7 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -31788,21 +29772,6 @@ "loose-envify": "^1.0.0" } }, - "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -31829,66 +29798,6 @@ "node": ">=12" } }, - "node_modules/webpack": { - "version": "5.106.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", - "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.16.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.20.0", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "loader-runner": "^4.3.1", - "mime-db": "^1.54.0", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.17", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.4" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.0.tgz", - "integrity": "sha512-gHwIe1cgBvvfLeu1Yz/dcFpmHfKDVxxyqI+kzqmuxZED81z2ChxpyqPaWcNqigPywhaEke7AjSGga+kxY55gjQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -31896,17 +29805,6 @@ "dev": true, "license": "MIT" }, - "node_modules/webpack/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", @@ -31961,6 +29859,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -32041,6 +29940,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, "license": "ISC" }, "node_modules/which-typed-array": { diff --git a/package.json b/package.json index 6d54dca10..614938ad2 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.1.0-alpha.4", + "@iqss/dataverse-client-javascript": "v2.2.0-pr446.56b9609", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", diff --git a/public/locales/en/shared.json b/public/locales/en/shared.json index e00b67b0b..25ed7ab9f 100644 --- a/public/locales/en/shared.json +++ b/public/locales/en/shared.json @@ -293,6 +293,8 @@ }, "downloadCitation": { "title": "Download Citation", + "citeDataset": "Cite Dataset", + "citeDataFile": "Cite Data File", "viewStyledCitation": "View Styled Citation", "downloadEndNoteXML": "Download EndNote XML", "downloadRIS": "Download RIS", @@ -300,6 +302,7 @@ "selectCSLStyle": "Select a CSL Style", "citationInStyle": "Citation in {{styleName}} style", "downloadError": "An error occurred while downloading the citation", + "downloadSuccess": "Citation downloaded successfully", "styledCitation": "Styled Citation" }, "expandableContent": { diff --git a/public/locales/es/shared.json b/public/locales/es/shared.json index d22e10dcd..1b0f58c3d 100644 --- a/public/locales/es/shared.json +++ b/public/locales/es/shared.json @@ -293,6 +293,8 @@ }, "downloadCitation": { "title": "Descargar cita", + "citeDataset": "Citar dataset", + "citeDataFile": "Citar archivo de datos", "viewStyledCitation": "Ver cita con estilo", "downloadEndNoteXML": "Descargar EndNote XML", "downloadRIS": "Descargar RIS", @@ -300,6 +302,7 @@ "selectCSLStyle": "Seleccionar un estilo CSL", "citationInStyle": "Cita en estilo {{styleName}}", "downloadError": "Ocurrió un error al descargar la cita", + "downloadSuccess": "Cita descargada correctamente", "styledCitation": "Cita con estilo" }, "expandableContent": { diff --git a/src/files/domain/models/FileCitation.ts b/src/files/domain/models/FileCitation.ts new file mode 100644 index 000000000..daa8a713f --- /dev/null +++ b/src/files/domain/models/FileCitation.ts @@ -0,0 +1,12 @@ +export enum FileCitationFormat { + Internal = 'Internal', + EndNote = 'EndNote', + RIS = 'RIS', + BibTeX = 'BibTeX', + CSLJson = 'CSL' +} + +export interface FormattedFileCitation { + content: string + contentType: string +} diff --git a/src/files/domain/repositories/FileRepository.ts b/src/files/domain/repositories/FileRepository.ts index e5761c21e..51a027b62 100644 --- a/src/files/domain/repositories/FileRepository.ts +++ b/src/files/domain/repositories/FileRepository.ts @@ -13,6 +13,7 @@ import { FileMetadataDTO } from '@/files/domain/useCases/DTOs/FileMetadataDTO' import { RestrictFileDTO } from '../useCases/restrictFileDTO' import { FileVersionSummarySubset } from '../models/FileVersionSummaryInfo' import { FileVersionPaginationInfo } from '../models/FileVersionPaginationInfo' +import { FileCitationFormat, FormattedFileCitation } from '../models/FileCitation' export interface FileRepository { getAllByDatasetPersistentId: ( @@ -37,6 +38,10 @@ export interface FileRepository { fileId: number | string, paginationInfo?: FileVersionPaginationInfo ) => Promise + getFileCitationByFormat: ( + fileId: number | string, + format: FileCitationFormat + ) => Promise getById: (id: number, datasetVersionNumber?: string) => Promise getMultipleFileDownloadUrl: (ids: number[], downloadMode: FileDownloadMode) => string getFileDownloadUrl: (id: number, downloadMode: FileDownloadMode) => string diff --git a/src/files/domain/useCases/getFileCitationByFormat.ts b/src/files/domain/useCases/getFileCitationByFormat.ts new file mode 100644 index 000000000..8182c27cc --- /dev/null +++ b/src/files/domain/useCases/getFileCitationByFormat.ts @@ -0,0 +1,10 @@ +import { FormattedFileCitation, FileCitationFormat } from '../models/FileCitation' +import { FileRepository } from '../repositories/FileRepository' + +export function getFileCitationByFormat( + fileRepository: FileRepository, + fileId: string | number, + format: FileCitationFormat +): Promise { + return fileRepository.getFileCitationByFormat(fileId, format) +} diff --git a/src/files/infrastructure/FileJSDataverseRepository.ts b/src/files/infrastructure/FileJSDataverseRepository.ts index 4ff853562..49da3dc9a 100644 --- a/src/files/infrastructure/FileJSDataverseRepository.ts +++ b/src/files/infrastructure/FileJSDataverseRepository.ts @@ -12,6 +12,8 @@ import { getDatasetFilesTotalDownloadSize, getFileAndDataset, getFileCitation, + getFileCitationByFormat as jsGetFileCitationByFormat, + FileCitationFormat as JSFileCitationFormat, getFileDataTables, getFileDownloadCount, getFileUserPermissions, @@ -47,6 +49,7 @@ import { FileMetadataDTO } from '@/files/domain/useCases/DTOs/FileMetadataDTO' import { JSDataverseReadErrorHandler } from '@/shared/helpers/JSDataverseReadErrorHandler' import { FileVersionSummarySubset } from '../domain/models/FileVersionSummaryInfo' import { FileVersionPaginationInfo } from '../domain/models/FileVersionPaginationInfo' +import { FileCitationFormat, FormattedFileCitation } from '../domain/models/FileCitation' const includeDeaccessioned = true @@ -259,6 +262,21 @@ export class FileJSDataverseRepository implements FileRepository { return getFileVersionSummaries.execute(fileId, paginationInfo?.pageSize, paginationInfo?.offset) } + getFileCitationByFormat( + fileId: number | string, + format: FileCitationFormat + ): Promise { + return jsGetFileCitationByFormat + .execute(fileId, FileJSDataverseRepository.toJSFileCitationFormat(format)) + .then((content) => ({ + content, + contentType: FileJSDataverseRepository.getCitationContentType(format) + })) + .catch((error: ReadError) => { + throw new Error(error.message) + }) + } + getById(id: number, datasetVersionNumber?: string): Promise { return FileJSDataverseRepository.getPermissionsById(id) .then((permissions) => { @@ -322,6 +340,38 @@ export class FileJSDataverseRepository implements FileRepository { }) } + private static getCitationContentType(format: FileCitationFormat): string { + switch (format) { + case FileCitationFormat.EndNote: + return 'application/xml' + case FileCitationFormat.RIS: + return 'application/x-research-info-systems' + case FileCitationFormat.BibTeX: + return 'application/x-bibtex' + case FileCitationFormat.CSLJson: + return 'application/vnd.citationstyles.csl+json' + case FileCitationFormat.Internal: + default: + return 'text/html' + } + } + + private static toJSFileCitationFormat(format: FileCitationFormat): JSFileCitationFormat { + switch (format) { + case FileCitationFormat.EndNote: + return JSFileCitationFormat.ENDNOTE + case FileCitationFormat.RIS: + return JSFileCitationFormat.RIS + case FileCitationFormat.BibTeX: + return JSFileCitationFormat.BIBTEX + case FileCitationFormat.CSLJson: + return JSFileCitationFormat.CSL + case FileCitationFormat.Internal: + default: + return JSFileCitationFormat.INTERNAL + } + } + getMultipleFileDownloadUrl(ids: number[], downloadMode: FileDownloadMode): string { return `/api/access/datafiles/${ids.join(',')}?format=${downloadMode}` } diff --git a/src/sections/file/File.tsx b/src/sections/file/File.tsx index dbe93ea03..120d024b2 100644 --- a/src/sections/file/File.tsx +++ b/src/sections/file/File.tsx @@ -135,7 +135,12 @@ export function File({ {t('fileCitationTitle')} - + {t('datasetCitationTitle')} } /> - + + + + ) diff --git a/src/sections/shared/citation/citation-download/CitationDownloadButton.tsx b/src/sections/shared/citation/citation-download/CitationDownloadButton.tsx index f15e3f946..ab85af419 100644 --- a/src/sections/shared/citation/citation-download/CitationDownloadButton.tsx +++ b/src/sections/shared/citation/citation-download/CitationDownloadButton.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'react-i18next' import { DropdownButton, DropdownButtonItem } from '@iqss/dataverse-design-system' import { ViewStyledCitationModal } from './ViewStyledCitationModal' -import { useState } from 'react' +import { startTransition, useEffect, useState } from 'react' import { CitationFormat } from '@/dataset/domain/models/DatasetCitation' import { DatasetRepository } from '@/dataset/domain/repositories/DatasetRepository' import { useDownloadCitation } from './useDownloadCitation' @@ -30,32 +30,43 @@ export function CitationDownloadButton({ version }) - if (error) { - toast.error(t('downloadError')) - } + useEffect(() => { + if (error) { + toast.error(t('downloadError')) + } + }, [error, t]) const handleCloseModal = () => setIsModalOpen(false) const handleOpenModal = async () => { - setIsModalOpen(true) + startTransition(() => setIsModalOpen(true)) const result = await handleGetCitation(CitationFormat.Internal) setStyledCitation(result) } + + const handleDownload = (format: CitationFormat, filename: string) => { + void handleDownloadCitation(format, filename).then((downloaded) => { + if (downloaded) { + toast.success(t('downloadSuccess')) + } + }) + } + return ( <> - + handleDownloadCitation(CitationFormat.EndNote, `${datasetId}.xml`)}> + onClick={() => handleDownload(CitationFormat.EndNote, `${datasetId}.xml`)}> {t('downloadEndNoteXML')} handleDownloadCitation(CitationFormat.RIS, `${datasetId}.ris`)}> + onClick={() => handleDownload(CitationFormat.RIS, `${datasetId}.ris`)}> {t('downloadRIS')} handleDownloadCitation(CitationFormat.BibTeX, `${datasetId}.bib`)}> + onClick={() => handleDownload(CitationFormat.BibTeX, `${datasetId}.bib`)}> {t('downloadBibTeX')} diff --git a/src/sections/shared/citation/citation-download/FileCitationDownloadButton.tsx b/src/sections/shared/citation/citation-download/FileCitationDownloadButton.tsx new file mode 100644 index 000000000..b36193215 --- /dev/null +++ b/src/sections/shared/citation/citation-download/FileCitationDownloadButton.tsx @@ -0,0 +1,58 @@ +import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { toast } from 'react-toastify' +import { DropdownButton, DropdownButtonItem } from '@iqss/dataverse-design-system' +import { FileRepository } from '@/files/domain/repositories/FileRepository' +import { FileCitationFormat } from '@/files/domain/models/FileCitation' +import { useDownloadFileCitation } from './useDownloadFileCitation' + +interface FileCitationDownloadButtonProps { + fileRepository: FileRepository + fileId: string | number +} + +export function FileCitationDownloadButton({ + fileRepository, + fileId +}: FileCitationDownloadButtonProps) { + const { t } = useTranslation('shared', { keyPrefix: 'downloadCitation' }) + + const { error, handleDownloadCitation } = useDownloadFileCitation({ + fileRepository, + fileId + }) + + useEffect(() => { + if (error) { + toast.error(t('downloadError')) + } + }, [error, t]) + + const handleDownload = (format: FileCitationFormat, filename: string) => { + void handleDownloadCitation(format, filename).then((downloaded) => { + if (downloaded) { + toast.success(t('downloadSuccess')) + } + }) + } + + return ( + + handleDownload(FileCitationFormat.EndNote, `${fileId}.xml`)}> + {t('downloadEndNoteXML')} + + handleDownload(FileCitationFormat.RIS, `${fileId}.ris`)}> + {t('downloadRIS')} + + handleDownload(FileCitationFormat.BibTeX, `${fileId}.bib`)}> + {t('downloadBibTeX')} + + + ) +} diff --git a/src/sections/shared/citation/citation-download/useDownloadCitation.tsx b/src/sections/shared/citation/citation-download/useDownloadCitation.tsx index 66f31b6f8..fed234d77 100644 --- a/src/sections/shared/citation/citation-download/useDownloadCitation.tsx +++ b/src/sections/shared/citation/citation-download/useDownloadCitation.tsx @@ -37,11 +37,16 @@ export function useDownloadCitation({ /** * Downloads a citation file after fetching it. */ - const handleDownloadCitation = async (format: CitationFormat, filename: string) => { + const handleDownloadCitation = async ( + format: CitationFormat, + filename: string + ): Promise => { const citation = await handleGetCitation(format) if (citation) { downloadFile(citation.content, filename, citation.contentType) + return true } + return false } return { isLoading, error, handleGetCitation, handleDownloadCitation } diff --git a/src/sections/shared/citation/citation-download/useDownloadFileCitation.tsx b/src/sections/shared/citation/citation-download/useDownloadFileCitation.tsx new file mode 100644 index 000000000..67b56f199 --- /dev/null +++ b/src/sections/shared/citation/citation-download/useDownloadFileCitation.tsx @@ -0,0 +1,45 @@ +import { useState } from 'react' +import { FileRepository } from '@/files/domain/repositories/FileRepository' +import { getFileCitationByFormat } from '@/files/domain/useCases/getFileCitationByFormat' +import { FileCitationFormat, FormattedFileCitation } from '@/files/domain/models/FileCitation' +import { downloadFile } from './useDownloadCitation' + +export function useDownloadFileCitation({ + fileRepository, + fileId +}: { + fileRepository: FileRepository + fileId: string | number +}) { + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const handleGetCitation = async ( + format: FileCitationFormat + ): Promise => { + setIsLoading(true) + setError(null) + try { + return await getFileCitationByFormat(fileRepository, fileId, format) + } catch (err) { + setError('Failed to fetch citation.') + return null + } finally { + setIsLoading(false) + } + } + + const handleDownloadCitation = async ( + format: FileCitationFormat, + filename: string + ): Promise => { + const citation = await handleGetCitation(format) + if (citation) { + downloadFile(citation.content, filename, citation.contentType) + return true + } + return false + } + + return { isLoading, error, handleGetCitation, handleDownloadCitation } +} diff --git a/src/stories/file/FileMockRepository.ts b/src/stories/file/FileMockRepository.ts index e4987b47a..4a4186590 100644 --- a/src/stories/file/FileMockRepository.ts +++ b/src/stories/file/FileMockRepository.ts @@ -19,6 +19,7 @@ import { FileMetadataDTO } from '@/files/domain/useCases/DTOs/FileMetadataDTO' import { RestrictFileDTO } from '@/files/domain/useCases/restrictFileDTO' import { FileVersionSummarySubset } from '@/files/domain/models/FileVersionSummaryInfo' import { FileVersionPaginationInfo } from '@/files/domain/models/FileVersionPaginationInfo' +import { FileCitationFormat, FormattedFileCitation } from '@/files/domain/models/FileCitation' export class FileMockRepository implements FileRepository { constructor(public readonly fileMock?: File) {} @@ -171,6 +172,20 @@ export class FileMockRepository implements FileRepository { }) } + getFileCitationByFormat( + _fileId: number | string, + _format: FileCitationFormat + ): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + content: 'Formatted file citation content', + contentType: 'text/plain' + }) + }, FakerHelper.loadingTimout()) + }) + } + updateCategories(_fileId: number | string, _categories: string[]): Promise { return new Promise((resolve) => { setTimeout(() => { diff --git a/src/stories/file/file-citation/FileCitation.stories.tsx b/src/stories/file/file-citation/FileCitation.stories.tsx index 3a7a84dd2..3a8d24666 100644 --- a/src/stories/file/file-citation/FileCitation.stories.tsx +++ b/src/stories/file/file-citation/FileCitation.stories.tsx @@ -3,6 +3,9 @@ import { WithI18next } from '../../WithI18next' import { FileCitation } from '../../../sections/file/file-citation/FileCitation' import { FileCitationMother } from '../../../../tests/component/files/domain/models/FileMother' import { DatasetVersionMother } from '../../../../tests/component/dataset/domain/models/DatasetMother' +import { FileMockRepository } from '../FileMockRepository' + +const fileRepository = new FileMockRepository() const meta: Meta = { title: 'Sections/File Page/FileCitation', @@ -21,6 +24,8 @@ export const Default: Story = { ) @@ -34,6 +39,8 @@ export const Draft: Story = { ) @@ -49,6 +56,8 @@ export const Deaccessioned: Story = { ) diff --git a/tests/component/sections/file/File.spec.tsx b/tests/component/sections/file/File.spec.tsx index 361dd80f0..dd42ea569 100644 --- a/tests/component/sections/file/File.spec.tsx +++ b/tests/component/sections/file/File.spec.tsx @@ -35,6 +35,7 @@ describe('File', () => { cy.findByText(`This file is part of "${testFile.datasetVersion.title}".`).should('exist') cy.findByText('File Citation').should('exist') cy.findByText(/fileName/).should('exist') + cy.findByText('Cite Data File').should('exist') cy.findByText('Dataset Citation').should('exist') cy.findByText('Cite Dataset').should('exist') cy.findAllByText(/Bennet, Elizabeth; Darcy, Fitzwilliam, 2023, "Dataset Title",/).should( diff --git a/tests/component/sections/file/file-citation/FileCitation.spec.tsx b/tests/component/sections/file/file-citation/FileCitation.spec.tsx index c9d0fb7fc..b7a23f835 100644 --- a/tests/component/sections/file/file-citation/FileCitation.spec.tsx +++ b/tests/component/sections/file/file-citation/FileCitation.spec.tsx @@ -1,24 +1,42 @@ import { FileCitation } from '../../../../../src/sections/file/file-citation/FileCitation' import { FileCitationMother } from '../../../files/domain/models/FileMother' import { DatasetVersionMother } from '../../../dataset/domain/models/DatasetMother' +import { FileRepository } from '@/files/domain/repositories/FileRepository' + +const fileRepository: FileRepository = {} as FileRepository describe('FileCitation', () => { it('renders the FileCitation', () => { const citation = FileCitationMother.create('File Title') const datasetVersion = DatasetVersionMother.createReleased() - cy.customMount() + cy.customMount( + + ) cy.findByText(/File Title/).should('exist') cy.findByText(/Bennet, Elizabeth; Darcy, Fitzwilliam, 2023, "Dataset Title",/).should('exist') cy.findByText(/RELEASED/).should('not.exist') cy.findByText(/V1/).should('exist') + cy.findByRole('button', { name: 'Cite Data File' }).should('exist') }) it('renders the FileCitation when the dataset is deaccessioned', () => { const citation = FileCitationMother.create('File Title') const datasetVersion = DatasetVersionMother.createDeaccessioned() - cy.customMount() + cy.customMount( + + ) cy.findByText(/File Title/).should('exist') cy.findByText(/Bennet, Elizabeth; Darcy, Fitzwilliam, 2023, "Dataset Title",/).should('exist') cy.findByRole('img', { name: 'tooltip icon' }).should('exist').trigger('mouseover') diff --git a/tests/component/sections/shared/citation/CitationDownloadButton.spec.tsx b/tests/component/sections/shared/citation/CitationDownloadButton.spec.tsx index e96902a1a..cf3ea1d3b 100644 --- a/tests/component/sections/shared/citation/CitationDownloadButton.spec.tsx +++ b/tests/component/sections/shared/citation/CitationDownloadButton.spec.tsx @@ -25,7 +25,7 @@ describe('CitationDownloadButton', () => { cy.findByRole('button', { name: 'Cite Dataset' }).should('exist') }) - it('downloads EndNote XML citation', () => { + it('downloads EndNote XML citation and shows success toast', () => { datasetRepository.getDatasetCitationInOtherFormats = cy.stub().resolves(mockCitation) cy.customMount( @@ -50,6 +50,7 @@ describe('CitationDownloadButton', () => { expect(win.URL['createObjectURL']).to.have.been.called expect(win.URL['revokeObjectURL']).to.have.been.called }) + cy.findByText('Citation downloaded successfully').should('exist') }) it('downloads RIS citation and triggers file download', () => { diff --git a/tests/component/sections/shared/citation/FileCitationDownloadButton.spec.tsx b/tests/component/sections/shared/citation/FileCitationDownloadButton.spec.tsx new file mode 100644 index 000000000..7f2441609 --- /dev/null +++ b/tests/component/sections/shared/citation/FileCitationDownloadButton.spec.tsx @@ -0,0 +1,63 @@ +import { FileRepository } from '@/files/domain/repositories/FileRepository' +import { FormattedFileCitation } from '@/files/domain/models/FileCitation' +import { FileCitationDownloadButton } from '@/sections/shared/citation/citation-download/FileCitationDownloadButton' + +const fileRepository: FileRepository = {} as FileRepository +const mockCitation: FormattedFileCitation = { + content: 'Mock File Citation', + contentType: 'text/plain' +} + +describe('FileCitationDownloadButton', () => { + beforeEach(() => { + cy.window().then((win) => { + cy.stub(win.URL, 'createObjectURL').returns('mock-url') + cy.stub(win.URL, 'revokeObjectURL') + }) + }) + + it('renders the Cite Data File button', () => { + cy.customMount() + cy.findByRole('button', { name: 'Cite Data File' }).should('exist') + }) + + it('opens the dropdown list and displays citation download options', () => { + fileRepository.getFileCitationByFormat = cy.stub().resolves(mockCitation) + + cy.customMount() + + cy.findByRole('button', { name: 'Cite Data File' }).click() + cy.findByText('Download EndNote XML').should('exist') + cy.findByText('Download RIS').should('exist') + cy.findByText('Download BibTeX').should('exist') + cy.findByText('View Styled Citation').should('not.exist') + }) + + it('downloads BibTeX file citation and shows success toast', () => { + fileRepository.getFileCitationByFormat = cy.stub().resolves(mockCitation) + + cy.customMount() + + cy.findByRole('button', { name: 'Cite Data File' }).click() + cy.findByText('Download BibTeX').click() + + cy.then(() => { + expect(fileRepository.getFileCitationByFormat).to.have.been.calledWith(3, 'BibTeX') + }) + cy.window().then((win) => { + expect(win.URL['createObjectURL']).to.have.been.called + expect(win.URL['revokeObjectURL']).to.have.been.called + }) + cy.findByText('Citation downloaded successfully').should('exist') + }) + + it('handles errors when downloading file citation', () => { + fileRepository.getFileCitationByFormat = cy.stub().rejects(new Error('Download error')) + + cy.customMount() + + cy.findByRole('button', { name: 'Cite Data File' }).click() + cy.findByText('Download EndNote XML').click() + cy.findByText('An error occurred while downloading the citation').should('exist') + }) +}) From 19b86240343c32120101ba7122c7ffa517deab2d Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 27 May 2026 11:06:08 -0400 Subject: [PATCH 02/15] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dab50f38c..bd1ca7d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added ### Changed + - File pages now include a "Cite Data File" dropdown for downloading file citations in EndNote XML, RIS, and BibTeX formats. ### Fixed From 4eecfebd73430408044ab39e4d6beac8785c6850 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 10 Jun 2026 10:50:46 -0400 Subject: [PATCH 03/15] update package --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a04509ca..626b6cc72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "v2.2.0-pr446.56b9609", + "@iqss/dataverse-client-javascript": "v2.2.0-alpha.8", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -1953,9 +1953,9 @@ } }, "node_modules/@iqss/dataverse-client-javascript": { - "version": "2.2.0-pr446.56b9609", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.2.0-pr446.56b9609/24962a5af33aeda8a69c6e5add63fa30bf962055", - "integrity": "sha512-LoO2giTz9eUxclH6q1FYXUEN717GYchUQGEeEsM9AMHKLF9nzGWNz4OcI0dj/0N+u+4LrB2xYpckT1jXBCVn2Q==", + "version": "2.2.0-alpha.8", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.2.0-alpha.8/7245f612cd7655f31d27b4c374bb736f530a7591", + "integrity": "sha512-y2p/UzQki2/Eib/lEYVVF5xzZRUHy1E+ZNvbTF3M6o6sABjza69tNaKkgSE5G7z6FZIXAB6/eO9a9/u3MnLH0g==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index 614938ad2..3e3a2be21 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "v2.2.0-pr446.56b9609", + "@iqss/dataverse-client-javascript": "v2.2.0-alpha.8", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", From 25e136844f3442bc7dc77cdeff8d5e989f8aff8e Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Tue, 23 Jun 2026 14:52:50 -0400 Subject: [PATCH 04/15] feat: add edit guestbook use case, and apply GuestbookDTO to both editGuestbook and createGuestbook --- src/guestbooks/domain/models/Guestbook.ts | 3 +- .../repositories/GuestbookRepository.ts | 5 ++-- .../domain/useCases/DTOs/GuestbookDTO.ts | 29 +++++++++++++++++++ .../domain/useCases/createGuestbook.ts | 4 +-- .../domain/useCases/editGuestbook.ts | 10 +++++++ .../useCases/getGuestbooksByCollectionId.ts | 15 ++++++++++ .../GuestbookJSDataverseRepository.ts | 16 ++++++---- .../DownloadWithTermsAndGuestbookModal.tsx | 2 +- .../create-guestbooks/CreateGuestbook.tsx | 4 +-- .../create-guestbooks/useCreateGuestbook.ts | 5 ++-- .../useGetGuestbooksByCollectionId.tsx | 4 ++- .../guestbook/GuestbookMockRepository.ts | 8 +++-- ...ownloadWithTermsAndGuestbookModal.spec.tsx | 1 + .../DatasetGuestbook.spec.tsx | 1 + .../EditDatasetTerms.spec.tsx | 1 + .../edit-dataset-terms/EditGuestbook.spec.tsx | 1 + .../useAssignDatasetGuestbook.spec.tsx | 1 + .../useRemoveDatasetGuestbook.spec.tsx | 1 + .../access-file-menu/AccessFileMenu.spec.tsx | 2 ++ .../guestbooks/ManageGuestbooks.spec.tsx | 1 + .../CreateGuestbook.spec.tsx | 9 +++--- .../createGuestbookRepositoryStub.ts | 1 + 22 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 src/guestbooks/domain/useCases/DTOs/GuestbookDTO.ts create mode 100644 src/guestbooks/domain/useCases/editGuestbook.ts create mode 100644 src/guestbooks/domain/useCases/getGuestbooksByCollectionId.ts diff --git a/src/guestbooks/domain/models/Guestbook.ts b/src/guestbooks/domain/models/Guestbook.ts index cc9744986..899ef793a 100644 --- a/src/guestbooks/domain/models/Guestbook.ts +++ b/src/guestbooks/domain/models/Guestbook.ts @@ -1,12 +1,13 @@ export type GuestbookQuestionType = 'text' | 'textarea' | 'options' export interface GuestbookOption { + id?: number value: string displayOrder: number } export interface GuestbookCustomQuestion { - id?: number | string + id?: number question: string required: boolean displayOrder: number diff --git a/src/guestbooks/domain/repositories/GuestbookRepository.ts b/src/guestbooks/domain/repositories/GuestbookRepository.ts index 1da6afee4..d2f1966b3 100644 --- a/src/guestbooks/domain/repositories/GuestbookRepository.ts +++ b/src/guestbooks/domain/repositories/GuestbookRepository.ts @@ -1,12 +1,13 @@ -import { type CreateGuestbookDTO } from '@iqss/dataverse-client-javascript' +import { GuestbookDTO } from '../useCases/DTOs/GuestbookDTO' import { Guestbook } from '../models/Guestbook' import { GuestbookResponseSubset } from '../models/GuestbookResponse' export interface GuestbookRepository { createGuestbook: ( collectionIdOrAlias: number | string, - guestbook: CreateGuestbookDTO + guestbook: GuestbookDTO ) => Promise + editGuestbook: (guestbookId: number, guestbook: GuestbookDTO) => Promise getGuestbook: (guestbookId: number) => Promise getGuestbooksByCollectionId: ( collectionIdOrAlias: number | string, diff --git a/src/guestbooks/domain/useCases/DTOs/GuestbookDTO.ts b/src/guestbooks/domain/useCases/DTOs/GuestbookDTO.ts new file mode 100644 index 000000000..9022b0572 --- /dev/null +++ b/src/guestbooks/domain/useCases/DTOs/GuestbookDTO.ts @@ -0,0 +1,29 @@ +import { GuestbookQuestionType } from '../../models/Guestbook' + +export interface GuestbookDTO { + id?: number + name: string + enabled: boolean + emailRequired: boolean + nameRequired: boolean + institutionRequired: boolean + positionRequired: boolean + createTime?: string + customQuestions: GuestbookCustomQuestionDTO[] +} + +export interface GuestbookCustomQuestionDTO { + id?: number + question: string + required: boolean + displayOrder: number + type: GuestbookQuestionType + hidden: boolean + optionValues?: GuestbookOptionDTO[] +} + +export interface GuestbookOptionDTO { + id?: number + value: string + displayOrder: number +} diff --git a/src/guestbooks/domain/useCases/createGuestbook.ts b/src/guestbooks/domain/useCases/createGuestbook.ts index 905d630cc..7d967f8a9 100644 --- a/src/guestbooks/domain/useCases/createGuestbook.ts +++ b/src/guestbooks/domain/useCases/createGuestbook.ts @@ -1,10 +1,10 @@ -import { type CreateGuestbookDTO } from '@iqss/dataverse-client-javascript' import { GuestbookRepository } from '../repositories/GuestbookRepository' +import { GuestbookDTO } from './DTOs/GuestbookDTO' export function createGuestbook( guestbookRepository: GuestbookRepository, collectionIdOrAlias: number | string, - guestbook: CreateGuestbookDTO + guestbook: GuestbookDTO ): Promise { return guestbookRepository.createGuestbook(collectionIdOrAlias, guestbook) } diff --git a/src/guestbooks/domain/useCases/editGuestbook.ts b/src/guestbooks/domain/useCases/editGuestbook.ts new file mode 100644 index 000000000..fe7c49762 --- /dev/null +++ b/src/guestbooks/domain/useCases/editGuestbook.ts @@ -0,0 +1,10 @@ +import { GuestbookRepository } from '../repositories/GuestbookRepository' +import { GuestbookDTO } from './DTOs/GuestbookDTO' + +export function editGuestbook( + guestbookRepository: GuestbookRepository, + guestbookId: number, + guestbook: GuestbookDTO +): Promise { + return guestbookRepository.editGuestbook(guestbookId, guestbook) +} diff --git a/src/guestbooks/domain/useCases/getGuestbooksByCollectionId.ts b/src/guestbooks/domain/useCases/getGuestbooksByCollectionId.ts new file mode 100644 index 000000000..2c2fcf05e --- /dev/null +++ b/src/guestbooks/domain/useCases/getGuestbooksByCollectionId.ts @@ -0,0 +1,15 @@ +import { Guestbook } from '../models/Guestbook' +import { GuestbookRepository } from '../repositories/GuestbookRepository' + +export function getGuestbooksByCollectionId( + guestbookRepository: GuestbookRepository, + collectionIdOrAlias: number | string, + includeStats?: boolean, + includeInherited?: boolean +): Promise { + return guestbookRepository.getGuestbooksByCollectionId( + collectionIdOrAlias, + includeStats, + includeInherited + ) +} diff --git a/src/guestbooks/infrastructure/repositories/GuestbookJSDataverseRepository.ts b/src/guestbooks/infrastructure/repositories/GuestbookJSDataverseRepository.ts index 2cd205084..97b812359 100644 --- a/src/guestbooks/infrastructure/repositories/GuestbookJSDataverseRepository.ts +++ b/src/guestbooks/infrastructure/repositories/GuestbookJSDataverseRepository.ts @@ -3,7 +3,9 @@ import { createGuestbook, downloadGuestbookResponsesByCollectionId, downloadGuestbookResponsesOfAGuestbook as downloadGuestbookResponsesByGuestbookId, - type CreateGuestbookDTO, + type CreateGuestbookDTO as JSDataverseCreateGuestbookDTO, + editGuestbook, + type EditGuestbookDTO as JSDataverseEditGuestbookDTO, type GuestbookResponseSubset, getGuestbooksByCollectionId, getGuestbook, @@ -13,13 +15,15 @@ import { } from '@iqss/dataverse-client-javascript' import { GuestbookRepository } from '../../domain/repositories/GuestbookRepository' import { Guestbook } from '../../domain/models/Guestbook' +import { GuestbookDTO } from '../../domain/useCases/DTOs/GuestbookDTO' export class GuestbookJSDataverseRepository implements GuestbookRepository { - createGuestbook( - collectionIdOrAlias: number | string, - guestbook: CreateGuestbookDTO - ): Promise { - return createGuestbook.execute(guestbook, collectionIdOrAlias) + createGuestbook(collectionIdOrAlias: number | string, guestbook: GuestbookDTO): Promise { + return createGuestbook.execute(guestbook as JSDataverseCreateGuestbookDTO, collectionIdOrAlias) + } + + editGuestbook(guestbookId: number, guestbook: GuestbookDTO): Promise { + return editGuestbook.execute(guestbookId, guestbook as JSDataverseEditGuestbookDTO) } getGuestbook(guestbookId: number): Promise { diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal.tsx index 9956e9d31..3e7b614b7 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/DownloadWithTermsAndGuestbookModal.tsx @@ -159,7 +159,7 @@ export function DownloadWithTermsAndGuestbookModal({ institution?: string position?: string } - const questionWithId = question as GuestbookCustomQuestion & { id?: number | string } + const questionWithId = question as GuestbookCustomQuestion & { id?: number } if (questionWithId?.id !== undefined) { return questionWithId.id diff --git a/src/sections/guestbooks/create-guestbooks/CreateGuestbook.tsx b/src/sections/guestbooks/create-guestbooks/CreateGuestbook.tsx index 67f10cc70..92e82a67c 100644 --- a/src/sections/guestbooks/create-guestbooks/CreateGuestbook.tsx +++ b/src/sections/guestbooks/create-guestbooks/CreateGuestbook.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' -import { type CreateGuestbookDTO } from '@iqss/dataverse-client-javascript' import { Alert, Button, Col, Form, Row } from '@iqss/dataverse-design-system' import { type NavigateFunction, useNavigate } from 'react-router-dom' import { DashLg, PlusLg } from 'react-bootstrap-icons' import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' import { GuestbookQuestionType } from '@/guestbooks/domain/models/Guestbook' +import { GuestbookDTO } from '@/guestbooks/domain/useCases/DTOs/GuestbookDTO' import { RouteWithParams } from '@/sections/Route.enum' import { useCollection } from '@/sections/collection/useCollection' import { NotFoundPage } from '@/sections/not-found-page/NotFoundPage' @@ -123,7 +123,7 @@ export const CreateGuestbook = ({ collectionId, collectionRepository }: CreateGu }) } - const buildGuestbookDTO = (): CreateGuestbookDTO => ({ + const buildGuestbookDTO = (): GuestbookDTO => ({ name: guestbookName.trim(), enabled: false, nameRequired, diff --git a/src/sections/guestbooks/create-guestbooks/useCreateGuestbook.ts b/src/sections/guestbooks/create-guestbooks/useCreateGuestbook.ts index 03f348d3b..a26e5e6c8 100644 --- a/src/sections/guestbooks/create-guestbooks/useCreateGuestbook.ts +++ b/src/sections/guestbooks/create-guestbooks/useCreateGuestbook.ts @@ -1,5 +1,6 @@ import { useState } from 'react' -import { type CreateGuestbookDTO, WriteError } from '@iqss/dataverse-client-javascript' +import { WriteError } from '@iqss/dataverse-client-javascript' +import { GuestbookDTO } from '@/guestbooks/domain/useCases/DTOs/GuestbookDTO' import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' import { createGuestbook } from '@/guestbooks/domain/useCases/createGuestbook' import { JSDataverseWriteErrorHandler } from '@/shared/helpers/JSDataverseWriteErrorHandler' @@ -18,7 +19,7 @@ export const useCreateGuestbook = ({ const [isCreatingGuestbook, setIsCreatingGuestbook] = useState(false) const [errorCreatingGuestbook, setErrorCreatingGuestbook] = useState(null) - const handleCreateGuestbook = async (guestbook: CreateGuestbookDTO) => { + const handleCreateGuestbook = async (guestbook: GuestbookDTO) => { setIsCreatingGuestbook(true) setErrorCreatingGuestbook(null) diff --git a/src/sections/guestbooks/useGetGuestbooksByCollectionId.tsx b/src/sections/guestbooks/useGetGuestbooksByCollectionId.tsx index c6659c2d4..112952f30 100644 --- a/src/sections/guestbooks/useGetGuestbooksByCollectionId.tsx +++ b/src/sections/guestbooks/useGetGuestbooksByCollectionId.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react' import { ReadError } from '@iqss/dataverse-client-javascript' import { Guestbook } from '@/guestbooks/domain/models/Guestbook' import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' +import { getGuestbooksByCollectionId } from '@/guestbooks/domain/useCases/getGuestbooksByCollectionId' import { JSDataverseReadErrorHandler } from '@/shared/helpers/JSDataverseReadErrorHandler' interface UseGetGuestbooksByCollectionIdProps { @@ -38,7 +39,8 @@ export const useGetGuestbooksByCollectionId = ({ setErrorGetGuestbooksByCollectionId(null) try { - const fetchedGuestbooks = await guestbookRepository.getGuestbooksByCollectionId( + const fetchedGuestbooks = await getGuestbooksByCollectionId( + guestbookRepository, collectionIdOrAlias, includeStats, includeInherited diff --git a/src/stories/shared-mock-repositories/guestbook/GuestbookMockRepository.ts b/src/stories/shared-mock-repositories/guestbook/GuestbookMockRepository.ts index fa39c51a9..7b9dd879b 100644 --- a/src/stories/shared-mock-repositories/guestbook/GuestbookMockRepository.ts +++ b/src/stories/shared-mock-repositories/guestbook/GuestbookMockRepository.ts @@ -1,10 +1,10 @@ import { - type CreateGuestbookDTO, type GuestbookResponseSubset, type Guestbook as JSDataverseGuestbook } from '@iqss/dataverse-client-javascript' import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' import { Guestbook } from '@/guestbooks/domain/models/Guestbook' +import { GuestbookDTO } from '@/guestbooks/domain/useCases/DTOs/GuestbookDTO' export const storybookGuestbook: Guestbook = { id: 3, @@ -45,10 +45,14 @@ export const storybookClientGuestbooks: JSDataverseGuestbook[] = [ ] export class GuestbookMockRepository implements GuestbookRepository { - createGuestbook(_collectionIdOrAlias: number | string, _guestbook: CreateGuestbookDTO) { + createGuestbook(_collectionIdOrAlias: number | string, _guestbook: GuestbookDTO) { return Promise.resolve(storybookGuestbook.id) } + editGuestbook(_guestbookId: number, _guestbook: GuestbookDTO): Promise { + return Promise.resolve() + } + getGuestbook(_guestbookId: number): Promise { return Promise.resolve(storybookGuestbook) } diff --git a/tests/component/sections/dataset/dataset-files/guestbook/DownloadWithTermsAndGuestbookModal.spec.tsx b/tests/component/sections/dataset/dataset-files/guestbook/DownloadWithTermsAndGuestbookModal.spec.tsx index 738f3237d..48a9c2574 100644 --- a/tests/component/sections/dataset/dataset-files/guestbook/DownloadWithTermsAndGuestbookModal.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/guestbook/DownloadWithTermsAndGuestbookModal.spec.tsx @@ -129,6 +129,7 @@ describe('DownloadWithTermsAndGuestbookModal', () => { guestbookRepository = { createGuestbook: cy.stub(), + editGuestbook: cy.stub(), getGuestbook: cy .stub() .as('getGuestbook') diff --git a/tests/component/sections/dataset/dataset-guestbook/DatasetGuestbook.spec.tsx b/tests/component/sections/dataset/dataset-guestbook/DatasetGuestbook.spec.tsx index e47171a66..55a5c94b7 100644 --- a/tests/component/sections/dataset/dataset-guestbook/DatasetGuestbook.spec.tsx +++ b/tests/component/sections/dataset/dataset-guestbook/DatasetGuestbook.spec.tsx @@ -25,6 +25,7 @@ describe('DatasetGuestbook', () => { beforeEach(() => { guestbookRepository = { createGuestbook: cy.stub(), + editGuestbook: cy.stub(), getGuestbook: cy.stub(), getGuestbooksByCollectionId: cy.stub(), getGuestbookResponsesByGuestbookId: cy.stub(), diff --git a/tests/component/sections/edit-dataset-terms/EditDatasetTerms.spec.tsx b/tests/component/sections/edit-dataset-terms/EditDatasetTerms.spec.tsx index c41e8f09a..7486692da 100644 --- a/tests/component/sections/edit-dataset-terms/EditDatasetTerms.spec.tsx +++ b/tests/component/sections/edit-dataset-terms/EditDatasetTerms.spec.tsx @@ -105,6 +105,7 @@ describe('EditDatasetTerms', () => { licenseRepository.getAvailableStandardLicenses = cy.stub().resolves(mockLicenses) guestbookRepository = { createGuestbook: cy.stub(), + editGuestbook: cy.stub(), getGuestbook: cy.stub(), getGuestbooksByCollectionId: cy.stub().resolves(mockGuestbooks), getGuestbookResponsesByGuestbookId: cy.stub(), diff --git a/tests/component/sections/edit-dataset-terms/EditGuestbook.spec.tsx b/tests/component/sections/edit-dataset-terms/EditGuestbook.spec.tsx index 99fd10791..68de4f70d 100644 --- a/tests/component/sections/edit-dataset-terms/EditGuestbook.spec.tsx +++ b/tests/component/sections/edit-dataset-terms/EditGuestbook.spec.tsx @@ -97,6 +97,7 @@ describe('EditGuestbook', () => { beforeEach(() => { guestbookRepository = { createGuestbook: cy.stub(), + editGuestbook: cy.stub(), getGuestbook: cy.stub(), getGuestbooksByCollectionId: cy.stub().resolves(mockGuestbooks), getGuestbookResponsesByGuestbookId: cy.stub(), diff --git a/tests/component/sections/edit-dataset-terms/useAssignDatasetGuestbook.spec.tsx b/tests/component/sections/edit-dataset-terms/useAssignDatasetGuestbook.spec.tsx index 7925d99e5..4d97eefaa 100644 --- a/tests/component/sections/edit-dataset-terms/useAssignDatasetGuestbook.spec.tsx +++ b/tests/component/sections/edit-dataset-terms/useAssignDatasetGuestbook.spec.tsx @@ -11,6 +11,7 @@ describe('useAssignDatasetGuestbook', () => { onSuccessfulAssignDatasetGuestbook = cy.stub().as('onSuccessfulAssignDatasetGuestbook') guestbookRepository = { createGuestbook: cy.stub(), + editGuestbook: cy.stub(), getGuestbook: cy.stub(), getGuestbooksByCollectionId: cy.stub(), getGuestbookResponsesByGuestbookId: cy.stub(), diff --git a/tests/component/sections/edit-dataset-terms/useRemoveDatasetGuestbook.spec.tsx b/tests/component/sections/edit-dataset-terms/useRemoveDatasetGuestbook.spec.tsx index fddb589af..498f7ca55 100644 --- a/tests/component/sections/edit-dataset-terms/useRemoveDatasetGuestbook.spec.tsx +++ b/tests/component/sections/edit-dataset-terms/useRemoveDatasetGuestbook.spec.tsx @@ -11,6 +11,7 @@ describe('useRemoveDatasetGuestbook', () => { onSuccessfulRemoveDatasetGuestbook = cy.stub().as('onSuccessfulRemoveDatasetGuestbook') guestbookRepository = { createGuestbook: cy.stub(), + editGuestbook: cy.stub(), getGuestbook: cy.stub(), getGuestbooksByCollectionId: cy.stub(), getGuestbookResponsesByGuestbookId: cy.stub(), diff --git a/tests/component/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.spec.tsx b/tests/component/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.spec.tsx index ab29b2d42..493fc7fe3 100644 --- a/tests/component/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.spec.tsx +++ b/tests/component/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.spec.tsx @@ -198,6 +198,7 @@ describe('AccessFileMenu', () => { it('opens the guestbook modal before starting the file download', () => { const guestbookRepository: GuestbookRepository = { createGuestbook: cy.stub().resolves(1), + editGuestbook: cy.stub().resolves(), getGuestbook: cy.stub().as('getGuestbook').resolves(guestbook), getGuestbooksByCollectionId: cy.stub().resolves([]), getGuestbookResponsesByGuestbookId: cy.stub(), @@ -334,6 +335,7 @@ describe('AccessFileMenu', () => { it('opens the terms modal when custom terms exist without a guestbook', () => { const guestbookRepository: GuestbookRepository = { createGuestbook: cy.stub().resolves(1), + editGuestbook: cy.stub().resolves(), getGuestbook: cy.stub().as('getGuestbook').resolves(guestbook), getGuestbooksByCollectionId: cy.stub().resolves([]), getGuestbookResponsesByGuestbookId: cy.stub(), diff --git a/tests/component/sections/guestbooks/ManageGuestbooks.spec.tsx b/tests/component/sections/guestbooks/ManageGuestbooks.spec.tsx index 45afe5c71..8b6791138 100644 --- a/tests/component/sections/guestbooks/ManageGuestbooks.spec.tsx +++ b/tests/component/sections/guestbooks/ManageGuestbooks.spec.tsx @@ -127,6 +127,7 @@ describe('ManageGuestbooks', () => { guestbookRepository = { createGuestbook: cy.stub(), + editGuestbook: cy.stub(), getGuestbook: cy.stub(), getGuestbooksByCollectionId: cy .stub() diff --git a/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx b/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx index 6de100531..8080e33e6 100644 --- a/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx +++ b/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx @@ -1,19 +1,20 @@ import { act, renderHook } from '@testing-library/react' -import { type CreateGuestbookDTO, WriteError } from '@iqss/dataverse-client-javascript' +import { WriteError } from '@iqss/dataverse-client-javascript' import { CreateGuestbook } from '@/sections/guestbooks/create-guestbooks/CreateGuestbook' import { useCreateGuestbook } from '@/sections/guestbooks/create-guestbooks/useCreateGuestbook' import { GuestbookRepositoryProvider } from '@/sections/guestbooks/GuestbookRepositoryProvider' import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' +import { GuestbookDTO } from '@/guestbooks/domain/useCases/DTOs/GuestbookDTO' import { CollectionMother } from '@tests/component/collection/domain/models/CollectionMother' import { createGuestbookRepositoryStub } from '../createGuestbookRepositoryStub' type CreateGuestbookStub = sinon.SinonStub< - [collectionIdOrAlias: number | string, guestbook: CreateGuestbookDTO], + [collectionIdOrAlias: number | string, guestbook: GuestbookDTO], Promise > -const guestbook: CreateGuestbookDTO = { +const guestbook: GuestbookDTO = { name: 'Test Guestbook', enabled: false, emailRequired: true, @@ -55,7 +56,7 @@ describe('CreateGuestbook', () => { ) - const expectGuestbookCreatedWith = (expectedGuestbook: CreateGuestbookDTO) => { + const expectGuestbookCreatedWith = (expectedGuestbook: GuestbookDTO) => { cy.wrap(null).should(() => { expect(createGuestbookStub).to.have.been.calledOnce diff --git a/tests/component/sections/guestbooks/createGuestbookRepositoryStub.ts b/tests/component/sections/guestbooks/createGuestbookRepositoryStub.ts index edbaef67d..8cef378a6 100644 --- a/tests/component/sections/guestbooks/createGuestbookRepositoryStub.ts +++ b/tests/component/sections/guestbooks/createGuestbookRepositoryStub.ts @@ -2,6 +2,7 @@ import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookR export const createGuestbookRepositoryStub = (): GuestbookRepository => ({ createGuestbook: cy.stub(), + editGuestbook: cy.stub(), getGuestbook: cy.stub(), getGuestbooksByCollectionId: cy.stub(), getGuestbookResponsesByGuestbookId: cy.stub(), From 5518176af3f3399864b4c67fe6cbac66dc1e5c82 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 24 Jun 2026 10:04:49 -0400 Subject: [PATCH 05/15] feat: edit guestbook integration --- CHANGELOG.md | 4 +- package-lock.json | 8 +- package.json | 2 +- public/locales/en/datasetTemplates.json | 2 +- public/locales/en/guestbooks.json | 8 +- public/locales/es/guestbooks.json | 8 +- .../GuestbookJSDataverseRepository.ts | 11 +- src/router/routes.tsx | 17 + src/sections/Route.enum.ts | 3 + src/sections/guestbooks/ManageGuestbooks.tsx | 6 + .../action-buttons/GuestbookActionButtons.tsx | 18 +- .../create-guestbooks/CreateGuestbook.tsx | 342 +-------------- .../edit-guestbook/EditGuestbook.tsx | 114 +++++ .../edit-guestbook/EditGuestbookFactory.tsx | 33 ++ .../edit-guestbook/useEditGuestbook.ts | 58 +++ .../guestbook-form/GuestbookForm.tsx | 398 ++++++++++++++++++ .../GuestbookActionButtons.spec.tsx | 44 ++ .../edit-guestbook/EditGuestbook.spec.tsx | 109 +++++ .../edit-guestbook/useEditGuestbook.spec.tsx | 166 ++++++++ .../guestbooks/ManageGuestbooks.spec.ts | 39 +- 20 files changed, 1043 insertions(+), 347 deletions(-) create mode 100644 src/sections/guestbooks/edit-guestbook/EditGuestbook.tsx create mode 100644 src/sections/guestbooks/edit-guestbook/EditGuestbookFactory.tsx create mode 100644 src/sections/guestbooks/edit-guestbook/useEditGuestbook.ts create mode 100644 src/sections/guestbooks/guestbook-form/GuestbookForm.tsx create mode 100644 tests/component/sections/guestbooks/edit-guestbook/EditGuestbook.spec.tsx create mode 100644 tests/component/sections/guestbooks/edit-guestbook/useEditGuestbook.spec.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index d51ac33c7..94fe06355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added - Manage Guestbooks page integration, including: - - a guestbooks table with sorting, enable/disable actions, preview, and per-guestbook response download + - a guestbooks table with sorting, edit, enable/disable actions, preview, and per-guestbook response download - Create Guestbook and Download All Responses actions - - the Create Guestbook page + - the Create and Edit Guestbook pages - a checkbox for including guestbooks from parent collections - Dataset Templates UI integration, including create/edit flows, previews, and skeleton states. diff --git a/package-lock.json b/package-lock.json index fd45160e3..99a8b2ada 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "v2.2.0-alpha.9", + "@iqss/dataverse-client-javascript": "v2.2.0-pr456.8edcbca", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -1953,9 +1953,9 @@ } }, "node_modules/@iqss/dataverse-client-javascript": { - "version": "2.2.0-alpha.9", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.2.0-alpha.9/56c08b2c94d32ffb79a3374b9607651363124fac", - "integrity": "sha512-D7ISknUYo1WFz93KzcLhmrIvqtZiAQX0loRppDrfAQgJoE5FHquFd2QnFQ0+jhism2v26CgZ1oqyYFP+p2BVFQ==", + "version": "2.2.0-pr456.8edcbca", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.2.0-pr456.8edcbca/ee7f76c05ce4d1402be99a70eb9cd9e1713b72fb", + "integrity": "sha512-OW6Za/jFpHnY647bGmsYETyeX0PR661BSVTxycSGUvbKlpDer/91RtJN0qiwdwW03ZSoanR4LwDeZAoBaD9vqA==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index 98023586e..20afbf7f7 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "v2.2.0-alpha.9", + "@iqss/dataverse-client-javascript": "v2.2.0-pr456.8edcbca", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", diff --git a/public/locales/en/datasetTemplates.json b/public/locales/en/datasetTemplates.json index 7f70cc561..b86d1a80d 100644 --- a/public/locales/en/datasetTemplates.json +++ b/public/locales/en/datasetTemplates.json @@ -8,7 +8,7 @@ "create": "Create Dataset Template", "makeDefault": "Make Default", "default": "Default", - "view": "View", + "view": "Preview", "copy": "Copy", "edit": "Edit Template", "delete": "Delete", diff --git a/public/locales/en/guestbooks.json b/public/locales/en/guestbooks.json index 447e5953b..90731c06d 100644 --- a/public/locales/en/guestbooks.json +++ b/public/locales/en/guestbooks.json @@ -11,17 +11,20 @@ "view": "View", "copy": "Copy", "edit": "Edit", + "delete": "Delete", "downloadResponses": "Download responses", "viewResponses": "View Responses" }, "errors": { "getGuestbook": "Something went wrong getting the guestbook. Try again later.", + "editGuestbook": "Something went wrong editing the guestbook. Try again later.", "toggleEnabled": "Something went wrong updating the guestbook status. Try again later.", "downloadResponses": "Something went wrong downloading guestbook responses. Try again later." }, "alerts": { "statusUpdated": "The guestbook status has been updated.", - "downloadStarted": "Your download has started." + "downloadStarted": "Your download has started.", + "updated": "The guestbook has been updated." }, "preview": { "title": "Preview Guestbook", @@ -94,5 +97,8 @@ }, "submit": "Create Dataset Guestbook", "cancel": "Cancel" + }, + "edit": { + "title": "Edit Dataset Guestbook" } } diff --git a/public/locales/es/guestbooks.json b/public/locales/es/guestbooks.json index e9c7fbd32..19a79ce8a 100644 --- a/public/locales/es/guestbooks.json +++ b/public/locales/es/guestbooks.json @@ -11,17 +11,20 @@ "view": "Ver", "copy": "Copiar", "edit": "Editar", + "delete": "Eliminar", "downloadResponses": "Descargar respuestas", "viewResponses": "Ver respuestas" }, "errors": { "getGuestbook": "Algo salió mal al obtener el libro de visitas. Inténtalo de nuevo más tarde.", + "editGuestbook": "Algo salió mal al editar el libro de visitas. Inténtalo de nuevo más tarde.", "toggleEnabled": "Algo salió mal al actualizar el estado del libro de visitas. Inténtalo de nuevo más tarde.", "downloadResponses": "Algo salió mal al descargar las respuestas del libro de visitas. Inténtalo de nuevo más tarde." }, "alerts": { "statusUpdated": "El estado del libro de visitas se ha actualizado.", - "downloadStarted": "La descarga ha comenzado." + "downloadStarted": "La descarga ha comenzado.", + "updated": "El libro de visitas se ha actualizado." }, "preview": { "title": "Vista previa del libro de visitas", @@ -94,5 +97,8 @@ }, "submit": "Crear libro de visitas del dataset", "cancel": "Cancelar" + }, + "edit": { + "title": "Editar libro de visitas del dataset" } } diff --git a/src/guestbooks/infrastructure/repositories/GuestbookJSDataverseRepository.ts b/src/guestbooks/infrastructure/repositories/GuestbookJSDataverseRepository.ts index 97b812359..46f3925f9 100644 --- a/src/guestbooks/infrastructure/repositories/GuestbookJSDataverseRepository.ts +++ b/src/guestbooks/infrastructure/repositories/GuestbookJSDataverseRepository.ts @@ -17,6 +17,11 @@ import { GuestbookRepository } from '../../domain/repositories/GuestbookReposito import { Guestbook } from '../../domain/models/Guestbook' import { GuestbookDTO } from '../../domain/useCases/DTOs/GuestbookDTO' +const normalizeGuestbook = (guestbook: Guestbook): Guestbook => ({ + ...guestbook, + customQuestions: guestbook.customQuestions ?? [] +}) + export class GuestbookJSDataverseRepository implements GuestbookRepository { createGuestbook(collectionIdOrAlias: number | string, guestbook: GuestbookDTO): Promise { return createGuestbook.execute(guestbook as JSDataverseCreateGuestbookDTO, collectionIdOrAlias) @@ -27,7 +32,9 @@ export class GuestbookJSDataverseRepository implements GuestbookRepository { } getGuestbook(guestbookId: number): Promise { - return getGuestbook.execute(guestbookId).then((guestbook) => guestbook as Guestbook) + return getGuestbook + .execute(guestbookId) + .then((guestbook) => normalizeGuestbook(guestbook as Guestbook)) } getGuestbooksByCollectionId( @@ -37,7 +44,7 @@ export class GuestbookJSDataverseRepository implements GuestbookRepository { ): Promise { return getGuestbooksByCollectionId .execute(collectionIdOrAlias, includeStats, includeInherited) - .then((guestbooks) => guestbooks as Guestbook[]) + .then((guestbooks) => (guestbooks as Guestbook[]).map(normalizeGuestbook)) } getGuestbookResponsesByGuestbookId( diff --git a/src/router/routes.tsx b/src/router/routes.tsx index 37d52d9fe..c5ac44f8d 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -150,6 +150,14 @@ const CreateGuestbookPage = lazy(() => ) ) +const EditGuestbookPage = lazy(() => + import('../sections/guestbooks/edit-guestbook/EditGuestbookFactory').then( + ({ EditGuestbookFactory }) => ({ + default: () => EditGuestbookFactory.create() + }) + ) +) + const NotFoundPage = lazy(() => import('../sections/not-found-page/NotFoundPageFactory').then(({ NotFoundPageFactory }) => ({ default: () => NotFoundPageFactory.create() @@ -356,6 +364,15 @@ export const routes: RouteObject[] = [ ), errorElement: }, + { + path: Route.GUESTBOOKS_EDIT, + element: ( + }> + + + ), + errorElement: + }, { path: Route.TEMPLATES_CREATE, element: ( diff --git a/src/sections/Route.enum.ts b/src/sections/Route.enum.ts index 642498b6c..92980e6cc 100644 --- a/src/sections/Route.enum.ts +++ b/src/sections/Route.enum.ts @@ -23,6 +23,7 @@ export enum Route { COLLECTION_TEMPLATES = '/:collectionId/templates', GUESTBOOKS = '/:collectionId/guestbooks', GUESTBOOKS_CREATE = '/:collectionId/guestbooks/create', + GUESTBOOKS_EDIT = '/:collectionId/guestbooks/:guestbookId/edit', TEMPLATES_CREATE = '/:collectionId/templates/create', TEMPLATES_EDIT_METADATA = '/:collectionId/templates/:templateId/edit/metadata', TEMPLATES_EDIT_TERMS = '/:collectionId/templates/:templateId/edit/terms', @@ -43,6 +44,8 @@ export const RouteWithParams = { COLLECTION_TEMPLATES: (collectionId: string) => `/${collectionId}/templates`, GUESTBOOKS: (collectionId: string) => `/${collectionId}/guestbooks`, GUESTBOOKS_CREATE: (collectionId: string) => `/${collectionId}/guestbooks/create`, + GUESTBOOKS_EDIT: (collectionId: string, guestbookId: number | string) => + `/${collectionId}/guestbooks/${guestbookId}/edit`, TEMPLATES_CREATE: (collectionId: string) => `/${collectionId}/templates/create`, TEMPLATES_EDIT_METADATA: (collectionId: string, templateId: number | string) => `/${collectionId}/templates/${templateId}/edit/metadata`, diff --git a/src/sections/guestbooks/ManageGuestbooks.tsx b/src/sections/guestbooks/ManageGuestbooks.tsx index fa250be7c..03e5a35ba 100644 --- a/src/sections/guestbooks/ManageGuestbooks.tsx +++ b/src/sections/guestbooks/ManageGuestbooks.tsx @@ -3,11 +3,13 @@ import { useTranslation } from 'react-i18next' import { Button, Form, Table, Alert } from '@iqss/dataverse-design-system' import { CaretDown, CaretUp, ChevronExpand, Download } from 'react-bootstrap-icons' import { toast } from 'react-toastify' +import { useNavigate } from 'react-router-dom' import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' import { Guestbook } from '@/guestbooks/domain/models/Guestbook' import { downloadGuestbookResponsesByCollectionId } from '@/guestbooks/domain/useCases/downloadGuestbookResponsesByCollectionId' import { downloadGuestbookResponsesByGuestbookId } from '@/guestbooks/domain/useCases/downloadGuestbookResponsesByGuestbookId' import { setGuestbookEnabled } from '@/guestbooks/domain/useCases/setGuestbookEnabled' +import { RouteWithParams } from '@/sections/Route.enum' import { useCollection } from '@/sections/collection/useCollection' import { NotFoundPage } from '@/sections/not-found-page/NotFoundPage' import { BreadcrumbsGenerator } from '@/sections/shared/hierarchy/BreadcrumbsGenerator' @@ -30,6 +32,7 @@ interface GuestbooksProps { export const Guestbooks = ({ collectionRepository, collectionId }: GuestbooksProps) => { const { t } = useTranslation('guestbooks') + const navigate = useNavigate() const [includeGuestbooksFromParent, setIncludeGuestbooksFromParent] = useState(true) const [sortBy, setSortBy] = useState<'name' | 'created' | 'usage' | 'responses' | null>(null) const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc') @@ -374,6 +377,9 @@ export const Guestbooks = ({ collectionRepository, collectionId }: GuestbooksPro setGuestbookToPreview(guestbook)} + onEdit={() => + navigate(RouteWithParams.GUESTBOOKS_EDIT(collectionId, guestbook.id)) + } onToggleEnabled={() => handleToggleEnabled(guestbook)} canToggleEnabled={isGuestbookFromCurrentCollection(guestbook)} canEdit={isGuestbookFromCurrentCollection(guestbook)} diff --git a/src/sections/guestbooks/action-buttons/GuestbookActionButtons.tsx b/src/sections/guestbooks/action-buttons/GuestbookActionButtons.tsx index 31f39486b..ff52a9e26 100644 --- a/src/sections/guestbooks/action-buttons/GuestbookActionButtons.tsx +++ b/src/sections/guestbooks/action-buttons/GuestbookActionButtons.tsx @@ -1,13 +1,14 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Button, ButtonGroup, Tooltip } from '@iqss/dataverse-design-system' -import { Download, Eye, Files, Pencil } from 'react-bootstrap-icons' +import { Download, Eye, Files, Pencil, Trash } from 'react-bootstrap-icons' import { NotImplementedModal } from '@/sections/not-implemented/NotImplementedModal' interface GuestbookActionButtonsProps { isEnabled: boolean onView: () => void onToggleEnabled: () => void + onEdit?: () => void canToggleEnabled?: boolean canEdit?: boolean isTogglingEnabled?: boolean @@ -21,6 +22,7 @@ export const GuestbookActionButtons = ({ isEnabled, onView, onToggleEnabled, + onEdit, canToggleEnabled = true, canEdit = true, isTogglingEnabled = false, @@ -64,7 +66,7 @@ export const GuestbookActionButtons = ({ @@ -80,6 +82,18 @@ export const GuestbookActionButtons = ({ + {!isEnabled && ( + + + + )} - - - - - - {question.type === 'options' && ( -
- - -
- - - - {t('create.fields.customQuestions.responseOptions')} - - - - {question.responseOptions.map((responseOption, optionIndex) => ( - - -
- - - - updateQuestion(question.id, (current) => ({ - ...current, - responseOptions: current.responseOptions.map((option, index) => - index === optionIndex ? event.target.value : option - ) - })) - } - /> - - -
- - -
- - - ))} -
- )} - - - updateQuestion(question.id, (current) => ({ - ...current, - required: !current.required - })) - } - /> - - {questionIndex !== customQuestions.length - 1 && ( -
- )} -
- ))} - - - -
- - -
- + ) } diff --git a/src/sections/guestbooks/edit-guestbook/EditGuestbook.tsx b/src/sections/guestbooks/edit-guestbook/EditGuestbook.tsx new file mode 100644 index 000000000..c0a92c152 --- /dev/null +++ b/src/sections/guestbooks/edit-guestbook/EditGuestbook.tsx @@ -0,0 +1,114 @@ +import { useEffect } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { Alert } from '@iqss/dataverse-design-system' +import { type NavigateFunction, useNavigate } from 'react-router-dom' +import { toast } from 'react-toastify' +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { GuestbookDTO } from '@/guestbooks/domain/useCases/DTOs/GuestbookDTO' +import { RouteWithParams } from '@/sections/Route.enum' +import { useCollection } from '@/sections/collection/useCollection' +import { NotFoundPage } from '@/sections/not-found-page/NotFoundPage' +import { BreadcrumbsGenerator } from '@/sections/shared/hierarchy/BreadcrumbsGenerator' +import { useLoading } from '@/shared/contexts/loading/LoadingContext' +import { useGetGuestbookById } from '@/sections/dataset/dataset-guestbook/useGetGuestbookById' +import { useGuestbookRepository } from '../GuestbookRepositoryContext' +import { GuestbookSkeleton } from '../GuestbookSkeleton' +import { GuestbookForm } from '../guestbook-form/GuestbookForm' +import { useEditGuestbook } from './useEditGuestbook' + +interface EditGuestbookProps { + collectionId: string + guestbookId: number + collectionRepository: CollectionRepository +} + +export const EditGuestbook = ({ + collectionId, + guestbookId, + collectionRepository +}: EditGuestbookProps) => { + const { t } = useTranslation('guestbooks') + const { t: tShared } = useTranslation('shared') + const navigate: NavigateFunction = useNavigate() + const guestbookRepository = useGuestbookRepository() + const { setIsLoading } = useLoading() + const { collection, isLoading } = useCollection(collectionRepository, collectionId) + const { guestbook, isLoadingGuestbook, errorGetGuestbook } = useGetGuestbookById({ + guestbookRepository, + guestbookId + }) + const guestbooksGuideUrl = + 'https://guides.dataverse.org/en/latest/user/dataverse-management.html#dataset-guestbooks' + const navigateToGuestbooks = () => navigate(RouteWithParams.GUESTBOOKS(collectionId)) + const { isEditingGuestbook, errorEditingGuestbook, handleEditGuestbook } = useEditGuestbook({ + guestbookRepository, + onSuccessfulEdit: () => { + toast.success(t('alerts.updated')) + navigateToGuestbooks() + } + }) + const isLoadingData = isLoading || isLoadingGuestbook + + useEffect(() => { + setIsLoading(isLoadingData) + }, [isLoadingData, setIsLoading]) + + const handleSubmit = (guestbookDTO: GuestbookDTO) => { + void handleEditGuestbook(guestbookId, guestbookDTO) + } + + if (!isLoading && !collection) { + return + } + + if (!isLoadingGuestbook && !guestbook && errorGetGuestbook === null) { + return + } + + if (isLoadingData || !collection || (!guestbook && errorGetGuestbook === null)) { + return + } + + return ( +
+ + + + + }} + /> + + + {errorGetGuestbook && {errorGetGuestbook}} + {errorEditingGuestbook && {errorEditingGuestbook}} + + {guestbook && ( + + )} +
+ ) +} diff --git a/src/sections/guestbooks/edit-guestbook/EditGuestbookFactory.tsx b/src/sections/guestbooks/edit-guestbook/EditGuestbookFactory.tsx new file mode 100644 index 000000000..debeb31a6 --- /dev/null +++ b/src/sections/guestbooks/edit-guestbook/EditGuestbookFactory.tsx @@ -0,0 +1,33 @@ +import { ReactElement } from 'react' +import { useParams } from 'react-router-dom' +import { CollectionJSDataverseRepository } from '@/collection/infrastructure/repositories/CollectionJSDataverseRepository' +import { NotFoundPage } from '@/sections/not-found-page/NotFoundPage' +import { EditGuestbook } from './EditGuestbook' + +const collectionRepository = new CollectionJSDataverseRepository() + +export class EditGuestbookFactory { + static create(): ReactElement { + return + } +} + +function EditGuestbookWithParams() { + const { collectionId, guestbookId } = useParams<{ + collectionId: string + guestbookId: string + }>() + const parsedGuestbookId = Number(guestbookId) + + if (!collectionId || !Number.isInteger(parsedGuestbookId)) { + return + } + + return ( + + ) +} diff --git a/src/sections/guestbooks/edit-guestbook/useEditGuestbook.ts b/src/sections/guestbooks/edit-guestbook/useEditGuestbook.ts new file mode 100644 index 000000000..5ed1e5b61 --- /dev/null +++ b/src/sections/guestbooks/edit-guestbook/useEditGuestbook.ts @@ -0,0 +1,58 @@ +import { useState } from 'react' +import { WriteError } from '@iqss/dataverse-client-javascript' +import { useTranslation } from 'react-i18next' +import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' +import { GuestbookDTO } from '@/guestbooks/domain/useCases/DTOs/GuestbookDTO' +import { editGuestbook } from '@/guestbooks/domain/useCases/editGuestbook' +import { JSDataverseWriteErrorHandler } from '@/shared/helpers/JSDataverseWriteErrorHandler' + +interface UseEditGuestbookProps { + guestbookRepository: GuestbookRepository + onSuccessfulEdit?: () => void +} + +const formatWriteErrorMessage = (error: WriteError): string => { + const errorHandler = new JSDataverseWriteErrorHandler(error) + + return errorHandler.getReasonWithoutStatusCode() ?? errorHandler.getErrorMessage() +} + +const isWriteErrorLike = (error: unknown): error is WriteError => + error instanceof WriteError || + (error instanceof Error && + error.message.includes('There was an error when writing the resource.')) + +export const useEditGuestbook = ({ + guestbookRepository, + onSuccessfulEdit +}: UseEditGuestbookProps) => { + const { t } = useTranslation('guestbooks') + const [isEditingGuestbook, setIsEditingGuestbook] = useState(false) + const [errorEditingGuestbook, setErrorEditingGuestbook] = useState(null) + + const handleEditGuestbook = async (guestbookId: number, guestbook: GuestbookDTO) => { + setIsEditingGuestbook(true) + setErrorEditingGuestbook(null) + + try { + await editGuestbook(guestbookRepository, guestbookId, guestbook) + onSuccessfulEdit?.() + } catch (err: WriteError | unknown) { + if (isWriteErrorLike(err)) { + setErrorEditingGuestbook(formatWriteErrorMessage(err)) + } else if (err instanceof Error && err.message) { + setErrorEditingGuestbook(err.message) + } else { + setErrorEditingGuestbook(t('errors.editGuestbook')) + } + } finally { + setIsEditingGuestbook(false) + } + } + + return { + isEditingGuestbook, + errorEditingGuestbook, + handleEditGuestbook + } +} diff --git a/src/sections/guestbooks/guestbook-form/GuestbookForm.tsx b/src/sections/guestbooks/guestbook-form/GuestbookForm.tsx new file mode 100644 index 000000000..84d32b695 --- /dev/null +++ b/src/sections/guestbooks/guestbook-form/GuestbookForm.tsx @@ -0,0 +1,398 @@ +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Button, Col, Form, Row } from '@iqss/dataverse-design-system' +import { DashLg, PlusLg } from 'react-bootstrap-icons' +import { Guestbook, GuestbookQuestionType } from '@/guestbooks/domain/models/Guestbook' +import { GuestbookDTO } from '@/guestbooks/domain/useCases/DTOs/GuestbookDTO' +import styles from '../create-guestbooks/CreateGuestbook.module.scss' + +interface GuestbookFormProps { + initialGuestbook?: Guestbook + isSubmitting: boolean + submitButtonText: string + cancelButtonText: string + onSubmit: (guestbook: GuestbookDTO) => void + onCancel: () => void +} + +interface CustomQuestionDraft { + draftId: number + id?: number + type: GuestbookQuestionType + questionText: string + required: boolean + hidden: boolean + responseOptions: ResponseOptionDraft[] +} + +interface ResponseOptionDraft { + id?: number + value: string +} + +const createEmptyQuestion = (draftId: number): CustomQuestionDraft => ({ + draftId, + type: 'text', + questionText: '', + required: false, + hidden: false, + responseOptions: [{ value: '' }] +}) + +const getInitialQuestions = (guestbook?: Guestbook): CustomQuestionDraft[] => { + const customQuestions = guestbook?.customQuestions ?? [] + + if (customQuestions.length === 0) { + return [createEmptyQuestion(1)] + } + + return customQuestions.map((question, index) => ({ + draftId: question.id ?? index + 1, + id: question.id, + type: question.type, + questionText: question.question, + required: question.required, + hidden: question.hidden, + responseOptions: + question.optionValues && question.optionValues.length > 0 + ? question.optionValues.map((option) => ({ id: option.id, value: option.value })) + : [{ value: '' }] + })) +} + +export const GuestbookForm = ({ + initialGuestbook, + isSubmitting, + submitButtonText, + cancelButtonText, + onSubmit, + onCancel +}: GuestbookFormProps) => { + const { t } = useTranslation('guestbooks') + const [guestbookName, setGuestbookName] = useState(initialGuestbook?.name ?? '') + const [nameRequired, setNameRequired] = useState(initialGuestbook?.nameRequired ?? false) + const [emailRequired, setEmailRequired] = useState(initialGuestbook?.emailRequired ?? false) + const [institutionRequired, setInstitutionRequired] = useState( + initialGuestbook?.institutionRequired ?? false + ) + const [positionRequired, setPositionRequired] = useState( + initialGuestbook?.positionRequired ?? false + ) + const [customQuestions, setCustomQuestions] = useState( + getInitialQuestions(initialGuestbook) + ) + + useEffect(() => { + setGuestbookName(initialGuestbook?.name ?? '') + setNameRequired(initialGuestbook?.nameRequired ?? false) + setEmailRequired(initialGuestbook?.emailRequired ?? false) + setInstitutionRequired(initialGuestbook?.institutionRequired ?? false) + setPositionRequired(initialGuestbook?.positionRequired ?? false) + setCustomQuestions(getInitialQuestions(initialGuestbook)) + }, [initialGuestbook]) + + const updateQuestion = ( + questionDraftId: number, + updater: (question: CustomQuestionDraft) => CustomQuestionDraft + ) => { + setCustomQuestions((current) => + current.map((question) => + question.draftId === questionDraftId ? updater(question) : question + ) + ) + } + + const addQuestionAfter = (questionDraftId: number) => { + setCustomQuestions((current) => { + const nextId = Math.max(...current.map((question) => question.draftId), 0) + 1 + const newQuestion = createEmptyQuestion(nextId) + const insertionIndex = current.findIndex((question) => question.draftId === questionDraftId) + + if (insertionIndex === -1) { + return [...current, newQuestion] + } + + const nextQuestions = [...current] + nextQuestions.splice(insertionIndex + 1, 0, newQuestion) + return nextQuestions + }) + } + + const removeQuestion = (questionDraftId: number) => { + setCustomQuestions((current) => { + if (current.length === 1) { + return current + } + return current.filter((question) => question.draftId !== questionDraftId) + }) + } + + const addOptionLine = (questionDraftId: number, optionIndex: number) => { + updateQuestion(questionDraftId, (question) => { + const nextOptions = [...question.responseOptions] + nextOptions.splice(optionIndex + 1, 0, { value: '' }) + return { ...question, responseOptions: nextOptions } + }) + } + + const removeOptionLine = (questionDraftId: number, optionIndex: number) => { + updateQuestion(questionDraftId, (question) => { + if (question.responseOptions.length === 1) { + return question + } + const nextOptions = question.responseOptions.filter((_, index) => index !== optionIndex) + return { ...question, responseOptions: nextOptions } + }) + } + + const buildGuestbookDTO = (): GuestbookDTO => ({ + ...(initialGuestbook?.id !== undefined ? { id: initialGuestbook.id } : {}), + name: guestbookName.trim(), + enabled: initialGuestbook?.enabled ?? false, + nameRequired, + emailRequired, + institutionRequired, + positionRequired, + ...(initialGuestbook?.createTime !== undefined + ? { createTime: initialGuestbook.createTime } + : {}), + customQuestions: customQuestions + .filter((question) => question.questionText.trim().length > 0) + .map((question, index) => ({ + ...(question.id !== undefined ? { id: question.id } : {}), + question: question.questionText.trim(), + required: question.required, + displayOrder: index, + type: question.type, + hidden: question.hidden, + optionValues: + question.type === 'options' + ? question.responseOptions + .filter((option) => option.value.trim().length > 0) + .map((option, optionIndex) => ({ + ...(option.id !== undefined ? { id: option.id } : {}), + value: option.value.trim(), + displayOrder: optionIndex + })) + : undefined + })) + }) + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault() + onSubmit(buildGuestbookDTO()) + } + + return ( +
+ + + {t('create.fields.name.label')} + + + setGuestbookName(event.target.value)} + /> + + + + + + {t('create.fields.dataCollected.label')} + + +

{t('create.fields.dataCollected.help')}

+
+ setNameRequired((current) => !current)} + /> + setEmailRequired((current) => !current)} + /> + setInstitutionRequired((current) => !current)} + /> + setPositionRequired((current) => !current)} + /> +
+ +
+ + + + {t('create.fields.customQuestions.label')} + + +

{t('create.fields.customQuestions.help')}

+ {customQuestions.map((question, questionIndex) => ( +
+ + + + {t('create.fields.customQuestions.typeLabel')} + + + updateQuestion(question.draftId, (current) => ({ + ...current, + type: event.target.value as GuestbookQuestionType + })) + }> + + + + + + + + {t('create.fields.customQuestions.questionText')} + + + updateQuestion(question.draftId, (current) => ({ + ...current, + questionText: event.target.value + })) + } + /> + + +
+ + +
+ +
+ + {question.type === 'options' && ( +
+ + +
+ + + + {t('create.fields.customQuestions.responseOptions')} + + + + {question.responseOptions.map((responseOption, optionIndex) => ( + + +
+ + + + updateQuestion(question.draftId, (current) => ({ + ...current, + responseOptions: current.responseOptions.map((option, index) => + index === optionIndex + ? { ...option, value: event.target.value } + : option + ) + })) + } + /> + + +
+ + +
+ + + ))} +
+ )} + + + updateQuestion(question.draftId, (current) => ({ + ...current, + required: !current.required + })) + } + /> + + {questionIndex !== customQuestions.length - 1 && ( +
+ )} +
+ ))} + + + +
+ + +
+ + ) +} diff --git a/tests/component/sections/guestbooks/GuestbookActionButtons.spec.tsx b/tests/component/sections/guestbooks/GuestbookActionButtons.spec.tsx index 009507ecb..213b7e7cb 100644 --- a/tests/component/sections/guestbooks/GuestbookActionButtons.spec.tsx +++ b/tests/component/sections/guestbooks/GuestbookActionButtons.spec.tsx @@ -31,6 +31,7 @@ const GuestbookActionButtonsTestWrapper = ({ isTogglingEnabled = false, isDownloadingResponses = false, onToggleEnabled = () => {}, + onEdit, onDownloadResponses = () => {} }: { isEnabled?: boolean @@ -39,6 +40,7 @@ const GuestbookActionButtonsTestWrapper = ({ isTogglingEnabled?: boolean isDownloadingResponses?: boolean onToggleEnabled?: () => void + onEdit?: () => void onDownloadResponses?: () => void }) => { const [showPreview, setShowPreview] = useState(false) @@ -49,6 +51,7 @@ const GuestbookActionButtonsTestWrapper = ({ isEnabled={isEnabled} onView={() => setShowPreview(true)} onToggleEnabled={onToggleEnabled} + onEdit={onEdit} canToggleEnabled={canToggleEnabled} canEdit={canEdit} isTogglingEnabled={isTogglingEnabled} @@ -80,6 +83,30 @@ describe('GuestbookActionButtons', () => { cy.findByRole('button', { name: 'Enable' }).should('exist') }) + it('shows the delete action only when the guestbook is disabled and places it after download responses', () => { + cy.customMount() + + cy.findByRole('button', { name: 'Delete' }).should('not.exist') + + cy.customMount() + + cy.findByRole('button', { name: 'Delete' }).should('exist') + cy.findByRole('group', { name: 'Action' }) + .find('button') + .then(($buttons) => { + const buttonNames = Array.from($buttons).map( + (button) => button.getAttribute('aria-label') ?? button.textContent?.trim() + ) + + expect(buttonNames.indexOf('Delete')).to.be.greaterThan( + buttonNames.indexOf('Download responses') + ) + expect(buttonNames.indexOf('Delete')).to.be.lessThan( + buttonNames.indexOf('View Responses') + ) + }) + }) + it('does not render the toggle button when toggling is not allowed', () => { cy.customMount() @@ -122,6 +149,15 @@ describe('GuestbookActionButtons', () => { cy.get('@onDownloadResponses').should('have.been.calledOnce') }) + it('triggers edit handler when supplied', () => { + const onEdit = cy.stub().as('onEdit') + + cy.customMount() + + cy.findByRole('button', { name: 'Edit' }).click() + cy.get('@onEdit').should('have.been.calledOnce') + }) + it('uses enabled button defaults when optional loading flags are omitted', () => { const onToggleEnabled = cy.stub().as('onToggleEnabled') const onDownloadResponses = cy.stub().as('onDownloadResponses') @@ -160,6 +196,14 @@ describe('GuestbookActionButtons', () => { cy.findByText('Not Implemented').should('exist') }) + it('opens the not implemented modal from the disabled guestbook delete button', () => { + cy.customMount() + + cy.findByRole('button', { name: 'Delete' }).click() + cy.findByText('Not Implemented').should('exist') + cy.findByText(/This feature is not implemented yet in the Modern version./i).should('exist') + }) + it('keeps toggle and download buttons enabled when loading flags are false', () => { cy.customMount( diff --git a/tests/component/sections/guestbooks/edit-guestbook/EditGuestbook.spec.tsx b/tests/component/sections/guestbooks/edit-guestbook/EditGuestbook.spec.tsx new file mode 100644 index 000000000..f7e7f55a9 --- /dev/null +++ b/tests/component/sections/guestbooks/edit-guestbook/EditGuestbook.spec.tsx @@ -0,0 +1,109 @@ +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { Guestbook } from '@/guestbooks/domain/models/Guestbook' +import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' +import { GuestbookRepositoryProvider } from '@/sections/guestbooks/GuestbookRepositoryProvider' +import { EditGuestbook } from '@/sections/guestbooks/edit-guestbook/EditGuestbook' +import { CollectionMother } from '@tests/component/collection/domain/models/CollectionMother' + +const guestbook: Guestbook = { + id: 10, + name: 'Research Guestbook', + enabled: true, + emailRequired: true, + nameRequired: false, + institutionRequired: true, + positionRequired: false, + customQuestions: [ + { + id: 55, + question: 'Preferred format', + required: true, + displayOrder: 0, + type: 'options', + hidden: false, + optionValues: [ + { id: 101, value: 'CSV', displayOrder: 0 }, + { id: 102, value: 'JSON', displayOrder: 1 } + ] + } + ], + createTime: '2026-01-01T00:00:00.000Z', + dataverseId: 17 +} + +describe('EditGuestbook', () => { + const collectionRepository = {} as CollectionRepository + let guestbookRepository: GuestbookRepository + + beforeEach(() => { + collectionRepository.getById = cy.stub().resolves( + CollectionMother.create({ + id: '17', + name: 'SubCollection' + }) + ) + guestbookRepository = { + createGuestbook: cy.stub(), + editGuestbook: cy.stub().as('editGuestbook').resolves(undefined), + getGuestbook: cy.stub().as('getGuestbook').resolves(guestbook), + getGuestbooksByCollectionId: cy.stub(), + getGuestbookResponsesByGuestbookId: cy.stub(), + setGuestbookEnabled: cy.stub(), + downloadGuestbookResponsesByCollectionId: cy.stub(), + downloadGuestbookResponsesByGuestbookId: cy.stub(), + assignDatasetGuestbook: cy.stub(), + removeDatasetGuestbook: cy.stub() + } + }) + + const mountComponent = () => + cy.customMount( + + + + ) + + it('prefills fields correctly', () => { + mountComponent() + + cy.findByDisplayValue('Research Guestbook').should('exist') + cy.findByLabelText('Email').should('be.checked') + cy.findByLabelText('Institution').should('be.checked') + cy.findByLabelText('Name').should('not.be.checked') + cy.findByLabelText('Position').should('not.be.checked') + cy.findByDisplayValue('Preferred format').should('exist') + cy.findByDisplayValue('CSV').should('exist') + cy.findByDisplayValue('JSON').should('exist') + cy.findByLabelText('Required field').should('be.checked') + + cy.get('@getGuestbook').should('have.been.calledOnceWith', guestbook.id) + cy.get('@editGuestbook').should('not.have.been.called') + }) + + it('shows success toast after save changes', () => { + mountComponent() + + cy.findByDisplayValue('Research Guestbook').clear().type('Updated Guestbook') + cy.findByRole('button', { name: 'Save Changes' }).click() + + cy.get('@editGuestbook').should('have.been.calledOnce') + cy.findByText('The guestbook has been updated.').should('exist') + }) + + it('renders when the guestbook response omits custom questions', () => { + ;(guestbookRepository.getGuestbook as Cypress.Agent).resolves({ + ...guestbook, + customQuestions: undefined + }) + + mountComponent() + + cy.findByDisplayValue('Research Guestbook').should('exist') + cy.findByRole('button', { name: 'Save Changes' }).should('exist') + cy.findByLabelText('Add question').should('exist') + }) +}) diff --git a/tests/component/sections/guestbooks/edit-guestbook/useEditGuestbook.spec.tsx b/tests/component/sections/guestbooks/edit-guestbook/useEditGuestbook.spec.tsx new file mode 100644 index 000000000..805f13bc7 --- /dev/null +++ b/tests/component/sections/guestbooks/edit-guestbook/useEditGuestbook.spec.tsx @@ -0,0 +1,166 @@ +import { act, renderHook, waitFor } from '@testing-library/react' +import { I18nextProvider } from 'react-i18next' +import i18next, { i18n as I18nInstance } from 'i18next' +import { initReactI18next } from 'react-i18next' +import { WriteError } from '@iqss/dataverse-client-javascript' +import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' +import { GuestbookDTO } from '@/guestbooks/domain/useCases/DTOs/GuestbookDTO' +import { useEditGuestbook } from '@/sections/guestbooks/edit-guestbook/useEditGuestbook' + +const guestbook: GuestbookDTO = { + id: 10, + name: 'Research Guestbook', + enabled: true, + emailRequired: true, + nameRequired: false, + institutionRequired: true, + positionRequired: false, + createTime: '2026-01-01T00:00:00.000Z', + customQuestions: [] +} + +const createI18n = (): I18nInstance => { + const instance = i18next.createInstance() + void instance.use(initReactI18next).init({ + lng: 'en', + fallbackLng: 'en', + ns: ['guestbooks'], + defaultNS: 'guestbooks', + initImmediate: false, + resources: { + en: { + guestbooks: { + errors: { + editGuestbook: 'Default edit guestbook error.' + } + } + } + } + }) + + return instance +} + +const createGuestbookRepositoryStub = (): GuestbookRepository => ({ + createGuestbook: cy.stub(), + editGuestbook: cy.stub(), + getGuestbook: cy.stub(), + getGuestbooksByCollectionId: cy.stub(), + getGuestbookResponsesByGuestbookId: cy.stub(), + setGuestbookEnabled: cy.stub(), + downloadGuestbookResponsesByCollectionId: cy.stub(), + downloadGuestbookResponsesByGuestbookId: cy.stub(), + assignDatasetGuestbook: cy.stub(), + removeDatasetGuestbook: cy.stub() +}) + +describe('useEditGuestbook', () => { + let i18n: I18nInstance + let guestbookRepository: GuestbookRepository + let onSuccessfulEdit: Cypress.Agent + + beforeEach(() => { + i18n = createI18n() + guestbookRepository = createGuestbookRepositoryStub() + onSuccessfulEdit = cy.stub().as('onSuccessfulEdit') + }) + + const renderUseEditGuestbook = () => + renderHook( + () => + useEditGuestbook({ + guestbookRepository, + onSuccessfulEdit + }), + { + wrapper: ({ children }) => {children} + } + ) + + it('initializes with default state', async () => { + const { result } = renderUseEditGuestbook() + + await waitFor(() => { + expect(result.current.isEditingGuestbook).to.equal(false) + expect(result.current.errorEditingGuestbook).to.equal(null) + }) + }) + + it('edits a guestbook and calls success callback', async () => { + const editGuestbookStub = guestbookRepository.editGuestbook as Cypress.Agent + editGuestbookStub.resolves(undefined) + + const { result } = renderUseEditGuestbook() + + await act(async () => { + await result.current.handleEditGuestbook(guestbook.id as number, guestbook) + }) + + expect(editGuestbookStub).to.have.been.calledOnceWith(guestbook.id, guestbook) + expect(onSuccessfulEdit).to.have.been.calledOnce + expect(result.current.isEditingGuestbook).to.equal(false) + expect(result.current.errorEditingGuestbook).to.equal(null) + }) + + it('sets formatted error when edit fails with WriteError', async () => { + const writeError = new WriteError('[400] Guestbook name already exists') + const editGuestbookStub = guestbookRepository.editGuestbook as Cypress.Agent + editGuestbookStub.rejects(writeError) + + const { result } = renderUseEditGuestbook() + + await act(async () => { + await result.current.handleEditGuestbook(guestbook.id as number, guestbook) + }) + + expect(onSuccessfulEdit).not.to.have.been.called + expect(result.current.errorEditingGuestbook).to.equal('Guestbook name already exists') + expect(result.current.isEditingGuestbook).to.equal(false) + }) + + it('sets formatted error when edit fails with WriteError-shaped Error', async () => { + const editGuestbookStub = guestbookRepository.editGuestbook as Cypress.Agent + editGuestbookStub.rejects( + new Error( + 'There was an error when writing the resource. Reason was: [409] Guestbook is in use' + ) + ) + + const { result } = renderUseEditGuestbook() + + await act(async () => { + await result.current.handleEditGuestbook(guestbook.id as number, guestbook) + }) + + expect(onSuccessfulEdit).not.to.have.been.called + expect(result.current.errorEditingGuestbook).to.equal('Guestbook is in use') + }) + + it('sets error message when edit fails with a regular Error', async () => { + const editGuestbookStub = guestbookRepository.editGuestbook as Cypress.Agent + editGuestbookStub.rejects(new Error('Network unavailable')) + + const { result } = renderUseEditGuestbook() + + await act(async () => { + await result.current.handleEditGuestbook(guestbook.id as number, guestbook) + }) + + expect(onSuccessfulEdit).not.to.have.been.called + expect(result.current.errorEditingGuestbook).to.equal('Network unavailable') + }) + + it('sets default error when edit fails with an unknown error', async () => { + const editGuestbookStub = guestbookRepository.editGuestbook as Cypress.Agent + editGuestbookStub.rejects('Error') + + const { result } = renderUseEditGuestbook() + + await act(async () => { + await result.current.handleEditGuestbook(guestbook.id as number, guestbook) + }) + + expect(onSuccessfulEdit).not.to.have.been.called + expect(result.current.errorEditingGuestbook).to.equal('Default edit guestbook error.') + }) +}) diff --git a/tests/e2e-integration/e2e/sections/guestbooks/ManageGuestbooks.spec.ts b/tests/e2e-integration/e2e/sections/guestbooks/ManageGuestbooks.spec.ts index a00d03fd8..f7d6b393b 100644 --- a/tests/e2e-integration/e2e/sections/guestbooks/ManageGuestbooks.spec.ts +++ b/tests/e2e-integration/e2e/sections/guestbooks/ManageGuestbooks.spec.ts @@ -66,7 +66,7 @@ describe('Manage Guestbooks', () => { cy.findByRole('button', { name: 'Create Dataset Guestbook' }).should('exist') }) - it('opens the not implemented modal from copy, edit, and view responses actions', () => { + it('opens the not implemented modal from copy and view responses actions', () => { const guestbookName = `E2E Manage Guestbook Actions ${Date.now()}` cy.wrap(GuestbookHelper.createAndGetByName(guestbookName), { timeout: 10000 }).then(() => { @@ -75,9 +75,6 @@ describe('Manage Guestbooks', () => { cy.contains('tbody tr', guestbookName).findByRole('button', { name: 'Copy' }).click() closeNotImplementedModal() - cy.contains('tbody tr', guestbookName).findByRole('button', { name: 'Edit' }).click() - closeNotImplementedModal() - cy.contains('tbody tr', guestbookName) .findByRole('button', { name: 'View Responses' }) .click() @@ -103,4 +100,38 @@ describe('Manage Guestbooks', () => { cy.findByText('Your download has started.').should('exist') }) }) + + it('edits a guestbook from the table edit action', () => { + const guestbookName = `E2E Edit Guestbook ${Date.now()}` + const updatedGuestbookName = `${guestbookName} Updated` + + cy.wrap(GuestbookHelper.createAndGetByName(guestbookName), { timeout: 10000 }).then( + (guestbook) => { + cy.visit(GUESTBOOKS_PAGE_URL) + + cy.contains('tbody tr', guestbookName).findByRole('button', { name: 'Edit' }).click() + + cy.url().should('include', `${FRONTEND_BASE_PATH}/root/guestbooks/${guestbook.id}/edit`) + cy.findByRole('link', { name: 'Root' }) + .closest('.breadcrumb') + .within(() => { + cy.findByText('Dataset Guestbooks').should('exist') + cy.findByText('Edit Dataset Guestbook').should('exist') + }) + cy.findByLabelText(/^Guestbook Name/).should('have.value', guestbookName) + + cy.findByLabelText(/^Guestbook Name/) + .clear() + .type(updatedGuestbookName) + cy.findByLabelText('Institution').check() + cy.findByRole('button', { name: 'Save Changes' }).click() + + cy.findByText('The guestbook has been updated.').should('exist') + cy.url().should('include', GUESTBOOKS_PAGE_URL) + cy.contains('tbody tr', updatedGuestbookName).within(() => { + cy.findByRole('button', { name: 'Edit' }).should('exist') + }) + } + ) + }) }) From b53808dae1e9bfda5dd09678496a41a47ff0b0e7 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 24 Jun 2026 14:54:08 -0400 Subject: [PATCH 06/15] feat: guestbook response page --- public/locales/en/guestbooks.json | 20 +- public/locales/es/guestbooks.json | 20 +- .../domain/models/GuestbookResponse.ts | 6 +- .../GuestbookResponsesPaginationInfo.ts | 7 + src/router/routes.tsx | 17 + src/sections/Route.enum.ts | 3 + .../guestbooks/Guestbooks.module.scss | 37 ++ src/sections/guestbooks/ManageGuestbooks.tsx | 3 + .../action-buttons/GuestbookActionButtons.tsx | 4 +- .../view-responses/GuestbookResponses.tsx | 336 ++++++++++++++++++ .../GuestbookResponsesFactory.tsx | 33 ++ .../useGetGuestbookResponsesByGuestbookId.ts | 81 +++++ .../GuestbookActionButtons.spec.tsx | 24 +- .../guestbooks/ManageGuestbooks.spec.tsx | 24 +- .../GuestbookResponses.spec.tsx | 221 ++++++++++++ .../templates/DatasetTemplates.spec.tsx | 12 +- .../guestbooks/ManageGuestbooks.spec.ts | 30 +- 17 files changed, 845 insertions(+), 33 deletions(-) create mode 100644 src/guestbooks/domain/models/GuestbookResponsesPaginationInfo.ts create mode 100644 src/sections/guestbooks/view-responses/GuestbookResponses.tsx create mode 100644 src/sections/guestbooks/view-responses/GuestbookResponsesFactory.tsx create mode 100644 src/sections/guestbooks/view-responses/useGetGuestbookResponsesByGuestbookId.ts create mode 100644 tests/component/sections/guestbooks/view-responses/GuestbookResponses.spec.tsx diff --git a/public/locales/en/guestbooks.json b/public/locales/en/guestbooks.json index 90731c06d..7510305c7 100644 --- a/public/locales/en/guestbooks.json +++ b/public/locales/en/guestbooks.json @@ -8,7 +8,7 @@ "enable": "Enable", "disable": "Disable", "downloadAllResponses": "Download All Responses", - "view": "View", + "view": "Preview", "copy": "Copy", "edit": "Edit", "delete": "Delete", @@ -18,6 +18,7 @@ "errors": { "getGuestbook": "Something went wrong getting the guestbook. Try again later.", "editGuestbook": "Something went wrong editing the guestbook. Try again later.", + "getGuestbookResponses": "Something went wrong getting guestbook responses. Try again later.", "toggleEnabled": "Something went wrong updating the guestbook status. Try again later.", "downloadResponses": "Something went wrong downloading guestbook responses. Try again later." }, @@ -100,5 +101,22 @@ }, "edit": { "title": "Edit Dataset Guestbook" + }, + "responses": { + "title": "Guestbook Responses", + "info": "Click \"Download Responses\" to download all collected responses for this guestbook, as a CSV file. To navigate and analyze your collected responses, we recommend importing this CSV file into Excel, Google Sheets or similar software.", + "guestbookName": "Guestbook Name", + "downloadButton": "Download Responses", + "count_one": "{{count}} Response", + "count_other": "{{count}} Responses", + "noRecords": "No records found.", + "table": { + "dataset": "Dataset", + "date": "Date", + "type": "Type", + "file": "File", + "user": "User", + "customQuestions": "Custom Questions" + } } } diff --git a/public/locales/es/guestbooks.json b/public/locales/es/guestbooks.json index 19a79ce8a..cccaa144f 100644 --- a/public/locales/es/guestbooks.json +++ b/public/locales/es/guestbooks.json @@ -8,7 +8,7 @@ "enable": "Habilitar", "disable": "Deshabilitar", "downloadAllResponses": "Descargar todas las respuestas", - "view": "Ver", + "view": "Vista previa", "copy": "Copiar", "edit": "Editar", "delete": "Eliminar", @@ -18,6 +18,7 @@ "errors": { "getGuestbook": "Algo salió mal al obtener el libro de visitas. Inténtalo de nuevo más tarde.", "editGuestbook": "Algo salió mal al editar el libro de visitas. Inténtalo de nuevo más tarde.", + "getGuestbookResponses": "Algo salió mal al obtener las respuestas del libro de visitas. Inténtalo de nuevo más tarde.", "toggleEnabled": "Algo salió mal al actualizar el estado del libro de visitas. Inténtalo de nuevo más tarde.", "downloadResponses": "Algo salió mal al descargar las respuestas del libro de visitas. Inténtalo de nuevo más tarde." }, @@ -100,5 +101,22 @@ }, "edit": { "title": "Editar libro de visitas del dataset" + }, + "responses": { + "title": "Respuestas del libro de visitas", + "info": "Haz clic en \"Descargar respuestas\" para descargar todas las respuestas recopiladas para este libro de visitas como archivo CSV. Para navegar y analizar las respuestas recopiladas, recomendamos importar este archivo CSV en Excel, Google Sheets o software similar.", + "guestbookName": "Nombre del libro de visitas", + "downloadButton": "Descargar respuestas", + "count_one": "{{count}} respuesta", + "count_other": "{{count}} respuestas", + "noRecords": "No se encontraron registros.", + "table": { + "dataset": "Dataset", + "date": "Fecha", + "type": "Tipo", + "file": "Fichero", + "user": "Usuario", + "customQuestions": "Preguntas personalizadas" + } } } diff --git a/src/guestbooks/domain/models/GuestbookResponse.ts b/src/guestbooks/domain/models/GuestbookResponse.ts index 6d915f8fd..e644ef6b6 100644 --- a/src/guestbooks/domain/models/GuestbookResponse.ts +++ b/src/guestbooks/domain/models/GuestbookResponse.ts @@ -4,13 +4,13 @@ export interface GuestbookResponse { datasetPid: string date: string type: EventType - fileName?: string + email?: string fileId?: number + fileName?: string filePid?: string - userName: string - email?: string institution?: string position?: string + userName: string customQuestions?: GuestbookResponseCustomQuestion[] } diff --git a/src/guestbooks/domain/models/GuestbookResponsesPaginationInfo.ts b/src/guestbooks/domain/models/GuestbookResponsesPaginationInfo.ts new file mode 100644 index 000000000..f494a2871 --- /dev/null +++ b/src/guestbooks/domain/models/GuestbookResponsesPaginationInfo.ts @@ -0,0 +1,7 @@ +import { PaginationInfo } from '@/shared/pagination/domain/models/PaginationInfo' + +export class GuestbookResponsesPaginationInfo extends PaginationInfo { + constructor(page = 1, pageSize = 10, totalItems = 0, itemName = 'Response') { + super(page, pageSize, totalItems, itemName) + } +} diff --git a/src/router/routes.tsx b/src/router/routes.tsx index c5ac44f8d..a34885529 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -158,6 +158,14 @@ const EditGuestbookPage = lazy(() => ) ) +const GuestbookResponsesPage = lazy(() => + import('../sections/guestbooks/view-responses/GuestbookResponsesFactory').then( + ({ GuestbookResponsesFactory }) => ({ + default: () => GuestbookResponsesFactory.create() + }) + ) +) + const NotFoundPage = lazy(() => import('../sections/not-found-page/NotFoundPageFactory').then(({ NotFoundPageFactory }) => ({ default: () => NotFoundPageFactory.create() @@ -373,6 +381,15 @@ export const routes: RouteObject[] = [ ), errorElement: }, + { + path: Route.GUESTBOOKS_RESPONSES, + element: ( + }> + + + ), + errorElement: + }, { path: Route.TEMPLATES_CREATE, element: ( diff --git a/src/sections/Route.enum.ts b/src/sections/Route.enum.ts index 92980e6cc..80deee503 100644 --- a/src/sections/Route.enum.ts +++ b/src/sections/Route.enum.ts @@ -24,6 +24,7 @@ export enum Route { GUESTBOOKS = '/:collectionId/guestbooks', GUESTBOOKS_CREATE = '/:collectionId/guestbooks/create', GUESTBOOKS_EDIT = '/:collectionId/guestbooks/:guestbookId/edit', + GUESTBOOKS_RESPONSES = '/:collectionId/guestbooks/:guestbookId/responses', TEMPLATES_CREATE = '/:collectionId/templates/create', TEMPLATES_EDIT_METADATA = '/:collectionId/templates/:templateId/edit/metadata', TEMPLATES_EDIT_TERMS = '/:collectionId/templates/:templateId/edit/terms', @@ -46,6 +47,8 @@ export const RouteWithParams = { GUESTBOOKS_CREATE: (collectionId: string) => `/${collectionId}/guestbooks/create`, GUESTBOOKS_EDIT: (collectionId: string, guestbookId: number | string) => `/${collectionId}/guestbooks/${guestbookId}/edit`, + GUESTBOOKS_RESPONSES: (collectionId: string, guestbookId: number | string) => + `/${collectionId}/guestbooks/${guestbookId}/responses`, TEMPLATES_CREATE: (collectionId: string) => `/${collectionId}/templates/create`, TEMPLATES_EDIT_METADATA: (collectionId: string, templateId: number | string) => `/${collectionId}/templates/${templateId}/edit/metadata`, diff --git a/src/sections/guestbooks/Guestbooks.module.scss b/src/sections/guestbooks/Guestbooks.module.scss index 4ed8768df..f5092412e 100644 --- a/src/sections/guestbooks/Guestbooks.module.scss +++ b/src/sections/guestbooks/Guestbooks.module.scss @@ -47,6 +47,33 @@ white-space: nowrap; } +.response-details { + margin: $spacer 0; +} + +.response-detail-row { + display: grid; + grid-template-columns: minmax(10rem, 18rem) minmax(0, 1fr); + gap: 1rem; + align-items: baseline; +} + +.response-actions { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + margin: $spacer 0; +} + +.no-records-cell { + text-align: left; +} + +.custom-questions-cell { + white-space: pre-line; +} + @media (max-width: 576px) { .table-actions { flex-direction: column; @@ -72,6 +99,16 @@ width: 100%; justify-content: center; } + + .response-detail-row { + grid-template-columns: 1fr; + gap: 0.25rem; + } + + .response-actions { + align-items: stretch; + flex-direction: column; + } } .create-button { diff --git a/src/sections/guestbooks/ManageGuestbooks.tsx b/src/sections/guestbooks/ManageGuestbooks.tsx index 03e5a35ba..3061badc6 100644 --- a/src/sections/guestbooks/ManageGuestbooks.tsx +++ b/src/sections/guestbooks/ManageGuestbooks.tsx @@ -380,6 +380,9 @@ export const Guestbooks = ({ collectionRepository, collectionId }: GuestbooksPro onEdit={() => navigate(RouteWithParams.GUESTBOOKS_EDIT(collectionId, guestbook.id)) } + onViewResponses={() => + navigate(RouteWithParams.GUESTBOOKS_RESPONSES(collectionId, guestbook.id)) + } onToggleEnabled={() => handleToggleEnabled(guestbook)} canToggleEnabled={isGuestbookFromCurrentCollection(guestbook)} canEdit={isGuestbookFromCurrentCollection(guestbook)} diff --git a/src/sections/guestbooks/action-buttons/GuestbookActionButtons.tsx b/src/sections/guestbooks/action-buttons/GuestbookActionButtons.tsx index ff52a9e26..8d8d27680 100644 --- a/src/sections/guestbooks/action-buttons/GuestbookActionButtons.tsx +++ b/src/sections/guestbooks/action-buttons/GuestbookActionButtons.tsx @@ -9,6 +9,7 @@ interface GuestbookActionButtonsProps { onView: () => void onToggleEnabled: () => void onEdit?: () => void + onViewResponses?: () => void canToggleEnabled?: boolean canEdit?: boolean isTogglingEnabled?: boolean @@ -23,6 +24,7 @@ export const GuestbookActionButtons = ({ onView, onToggleEnabled, onEdit, + onViewResponses, canToggleEnabled = true, canEdit = true, isTogglingEnabled = false, @@ -97,7 +99,7 @@ export const GuestbookActionButtons = ({ diff --git a/src/sections/guestbooks/view-responses/GuestbookResponses.tsx b/src/sections/guestbooks/view-responses/GuestbookResponses.tsx new file mode 100644 index 000000000..a224505ba --- /dev/null +++ b/src/sections/guestbooks/view-responses/GuestbookResponses.tsx @@ -0,0 +1,336 @@ +import { useEffect, useMemo, useState } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { Alert, Button, Table } from '@iqss/dataverse-design-system' +import { CaretDown, CaretUp, ChevronExpand, Download } from 'react-bootstrap-icons' +import { toast } from 'react-toastify' +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { GuestbookResponsesPaginationInfo } from '@/guestbooks/domain/models/GuestbookResponsesPaginationInfo' +import { EventType, GuestbookResponse } from '@/guestbooks/domain/models/GuestbookResponse' +import { downloadGuestbookResponsesByGuestbookId } from '@/guestbooks/domain/useCases/downloadGuestbookResponsesByGuestbookId' +import { useCollection } from '@/sections/collection/useCollection' +import { NotFoundPage } from '@/sections/not-found-page/NotFoundPage' +import { RouteWithParams } from '@/sections/Route.enum' +import { PaginationControls } from '@/sections/shared/pagination/PaginationControls' +import { BreadcrumbsGenerator } from '@/sections/shared/hierarchy/BreadcrumbsGenerator' +import { SeparationLine } from '@/sections/shared/layout/SeparationLine/SeparationLine' +import { downloadFile } from '@/sections/shared/citation/citation-download/useDownloadCitation' +import { useGetGuestbookById } from '@/sections/dataset/dataset-guestbook/useGetGuestbookById' +import { useLoading } from '@/shared/contexts/loading/LoadingContext' +import { GuestbookSkeleton } from '../GuestbookSkeleton' +import { useGuestbookRepository } from '../GuestbookRepositoryContext' +import { useGetGuestbookResponsesByGuestbookId } from './useGetGuestbookResponsesByGuestbookId' +import styles from '../Guestbooks.module.scss' + +interface GuestbookResponsesProps { + collectionRepository: CollectionRepository + collectionId: string + guestbookId: number +} + +type SortableColumn = 'dataset' | 'date' | 'type' | 'file' | 'user' + +const eventTypeLabels: Record = { + [EventType.ACCESS_REQUEST]: 'Access Request', + [EventType.DOWNLOAD]: 'Download', + [EventType.SUBSET]: 'Subset', + [EventType.EXPLORE]: 'Explore' +} + +const formatResponseDate = (date: string): string => new Date(date).toLocaleDateString() + +const getCustomQuestionsText = (response: GuestbookResponse): string => { + if (!response.customQuestions || response.customQuestions.length === 0) { + return '' + } + + return response.customQuestions + .map((customQuestion) => `${customQuestion.question}: ${customQuestion.response}`) + .join('\n') +} + +export const GuestbookResponses = ({ + collectionRepository, + collectionId, + guestbookId +}: GuestbookResponsesProps) => { + const { t } = useTranslation('guestbooks') + const [sortBy, setSortBy] = useState(null) + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc') + const [isDownloadingResponses, setIsDownloadingResponses] = useState(false) + const [downloadResponsesError, setDownloadResponsesError] = useState(null) + const [paginationInfo, setPaginationInfo] = useState( + new GuestbookResponsesPaginationInfo() + ) + const { setIsLoading } = useLoading() + const guestbookRepository = useGuestbookRepository() + const { collection, isLoading } = useCollection(collectionRepository, collectionId) + const { guestbook, isLoadingGuestbook, errorGetGuestbook } = useGetGuestbookById({ + guestbookRepository, + guestbookId + }) + const { + guestbookResponses, + totalGuestbookResponseCount, + isLoadingGuestbookResponses, + errorGetGuestbookResponses + } = useGetGuestbookResponsesByGuestbookId({ + guestbookRepository, + guestbookId, + limit: paginationInfo.pageSize, + offset: paginationInfo.offset + }) + const isLoadingData = isLoading || isLoadingGuestbook || isLoadingGuestbookResponses + + useEffect(() => { + setIsLoading(isLoadingData) + }, [isLoadingData, setIsLoading]) + + useEffect(() => { + setPaginationInfo((currentPaginationInfo) => + currentPaginationInfo.withTotal(totalGuestbookResponseCount) + ) + }, [totalGuestbookResponseCount]) + + const sortedGuestbookResponses = useMemo(() => { + if (!sortBy) { + return guestbookResponses + } + + const sorted = [...guestbookResponses] + sorted.sort((first, second) => { + if (sortBy === 'dataset') { + return first.dataset.localeCompare(second.dataset, undefined, { sensitivity: 'base' }) + } + if (sortBy === 'date') { + return new Date(first.date).getTime() - new Date(second.date).getTime() + } + if (sortBy === 'type') { + return getEventTypeLabel(first.type).localeCompare(getEventTypeLabel(second.type)) + } + if (sortBy === 'file') { + return (first.fileName ?? '').localeCompare(second.fileName ?? '', undefined, { + sensitivity: 'base' + }) + } + return first.userName.localeCompare(second.userName, undefined, { + sensitivity: 'base' + }) + }) + + return sortDirection === 'asc' ? sorted : sorted.reverse() + }, [guestbookResponses, sortBy, sortDirection]) + + const handleSort = (column: SortableColumn) => { + if (sortBy === column) { + setSortDirection((current) => (current === 'asc' ? 'desc' : 'asc')) + return + } + + setSortBy(column) + setSortDirection('asc') + } + + const sortIndicator = (column: SortableColumn) => { + if (sortBy === column) { + return sortDirection === 'asc' ? ( + + ) : ( + + ) + } + return + } + + const sortButtonClass = (column: SortableColumn) => + `${styles['sort-button']}${sortBy === column ? ` ${styles['sort-button-active']}` : ''}` + const sortHeaderClass = (column: SortableColumn) => + sortBy === column ? styles['sort-header-active'] : '' + + const handleDownloadResponses = async () => { + if (!guestbook) { + return + } + + setDownloadResponsesError(null) + setIsDownloadingResponses(true) + + try { + const csvContent = await downloadGuestbookResponsesByGuestbookId( + guestbookRepository, + guestbook.dataverseId, + guestbook.id + ) + downloadFile(csvContent, `${guestbook.name}-responses.csv`, 'text/csv;charset=utf-8') + toast.success(t('alerts.downloadStarted')) + } catch { + setDownloadResponsesError(t('errors.downloadResponses')) + } finally { + setIsDownloadingResponses(false) + } + } + + if (!isLoading && !collection) { + return + } + + if (!isLoadingGuestbook && !guestbook && errorGetGuestbook === null) { + return + } + + if (isLoadingData || !collection || (!guestbook && errorGetGuestbook === null)) { + return + } + + return ( +
+ + +
+
+

{collection.name}

+ {collection.affiliation ? ( + ({collection.affiliation}) + ) : null} +
+
+ + + + + + {errorGetGuestbook && {errorGetGuestbook}} + {errorGetGuestbookResponses && {errorGetGuestbookResponses}} + {downloadResponsesError && {downloadResponsesError}} + + {guestbook && ( + <> +
+
+ {t('responses.guestbookName')} + {guestbook.name} +
+
+ + + +
+ + {t('responses.count', { + count: totalGuestbookResponseCount + })} + + +
+ + + + + + + + + + + + + + {sortedGuestbookResponses.length === 0 ? ( + + + + ) : ( + sortedGuestbookResponses.map((response) => ( + + + + + + + + + )) + )} + +
+ + + + + + + + + + {t('responses.table.customQuestions')}
+ {t('responses.noRecords')} +
{response.dataset}{formatResponseDate(response.date)}{getEventTypeLabel(response.type)}{response.fileName ?? ''}{response.userName} + {getCustomQuestionsText(response)} +
+ {paginationInfo.totalPages > 1 && ( + + )} + + )} +
+ ) +} + +const getEventTypeLabel = (eventType: EventType): string => eventTypeLabels[eventType] ?? eventType diff --git a/src/sections/guestbooks/view-responses/GuestbookResponsesFactory.tsx b/src/sections/guestbooks/view-responses/GuestbookResponsesFactory.tsx new file mode 100644 index 000000000..50224d2b0 --- /dev/null +++ b/src/sections/guestbooks/view-responses/GuestbookResponsesFactory.tsx @@ -0,0 +1,33 @@ +import { ReactElement } from 'react' +import { useParams } from 'react-router-dom' +import { CollectionJSDataverseRepository } from '@/collection/infrastructure/repositories/CollectionJSDataverseRepository' +import { NotFoundPage } from '@/sections/not-found-page/NotFoundPage' +import { GuestbookResponses } from './GuestbookResponses' + +const collectionRepository = new CollectionJSDataverseRepository() + +export class GuestbookResponsesFactory { + static create(): ReactElement { + return + } +} + +function GuestbookResponsesWithParams() { + const { collectionId, guestbookId } = useParams<{ + collectionId: string + guestbookId: string + }>() + const parsedGuestbookId = Number(guestbookId) + + if (!collectionId || !Number.isInteger(parsedGuestbookId)) { + return + } + + return ( + + ) +} diff --git a/src/sections/guestbooks/view-responses/useGetGuestbookResponsesByGuestbookId.ts b/src/sections/guestbooks/view-responses/useGetGuestbookResponsesByGuestbookId.ts new file mode 100644 index 000000000..ce3908968 --- /dev/null +++ b/src/sections/guestbooks/view-responses/useGetGuestbookResponsesByGuestbookId.ts @@ -0,0 +1,81 @@ +import { useCallback, useEffect, useState } from 'react' +import { ReadError } from '@iqss/dataverse-client-javascript' +import { useTranslation } from 'react-i18next' +import { + GuestbookResponse, + GuestbookResponseSubset +} from '@/guestbooks/domain/models/GuestbookResponse' +import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' +import { getGuestbookResponsesByGuestbookId } from '@/guestbooks/domain/useCases/getGuestbookResponsesByGuestbookId' +import { JSDataverseReadErrorHandler } from '@/shared/helpers/JSDataverseReadErrorHandler' + +interface UseGetGuestbookResponsesByGuestbookIdProps { + guestbookRepository: GuestbookRepository + guestbookId?: number + limit?: number + offset?: number +} + +export const useGetGuestbookResponsesByGuestbookId = ({ + guestbookRepository, + guestbookId, + limit, + offset +}: UseGetGuestbookResponsesByGuestbookIdProps) => { + const { t } = useTranslation('guestbooks') + const [guestbookResponses, setGuestbookResponses] = useState([]) + const [totalGuestbookResponseCount, setTotalGuestbookResponseCount] = useState(0) + const [isLoadingGuestbookResponses, setIsLoadingGuestbookResponses] = useState( + guestbookId !== undefined + ) + const [errorGetGuestbookResponses, setErrorGetGuestbookResponses] = useState(null) + + const fetchGuestbookResponses = useCallback(async () => { + if (guestbookId === undefined) { + setGuestbookResponses([]) + setTotalGuestbookResponseCount(0) + setIsLoadingGuestbookResponses(false) + setErrorGetGuestbookResponses(null) + return + } + + setIsLoadingGuestbookResponses(true) + setErrorGetGuestbookResponses(null) + + try { + const responseSubset: GuestbookResponseSubset = await getGuestbookResponsesByGuestbookId( + guestbookRepository, + guestbookId, + limit, + offset + ) + setGuestbookResponses(responseSubset.guestbookResponses ?? []) + setTotalGuestbookResponseCount(responseSubset.totalGuestbookResponseCount ?? 0) + } catch (err) { + setGuestbookResponses([]) + setTotalGuestbookResponseCount(0) + if (err instanceof ReadError) { + const error = new JSDataverseReadErrorHandler(err) + const formattedError = + error.getReasonWithoutStatusCode() ?? /* istanbul ignore next */ error.getErrorMessage() + setErrorGetGuestbookResponses(formattedError) + } else { + setErrorGetGuestbookResponses(t('errors.getGuestbookResponses')) + } + } finally { + setIsLoadingGuestbookResponses(false) + } + }, [guestbookId, guestbookRepository, limit, offset, t]) + + useEffect(() => { + void fetchGuestbookResponses() + }, [fetchGuestbookResponses]) + + return { + guestbookResponses, + totalGuestbookResponseCount, + isLoadingGuestbookResponses, + errorGetGuestbookResponses, + fetchGuestbookResponses + } +} diff --git a/tests/component/sections/guestbooks/GuestbookActionButtons.spec.tsx b/tests/component/sections/guestbooks/GuestbookActionButtons.spec.tsx index 213b7e7cb..e8c827063 100644 --- a/tests/component/sections/guestbooks/GuestbookActionButtons.spec.tsx +++ b/tests/component/sections/guestbooks/GuestbookActionButtons.spec.tsx @@ -32,6 +32,7 @@ const GuestbookActionButtonsTestWrapper = ({ isDownloadingResponses = false, onToggleEnabled = () => {}, onEdit, + onViewResponses, onDownloadResponses = () => {} }: { isEnabled?: boolean @@ -41,6 +42,7 @@ const GuestbookActionButtonsTestWrapper = ({ isDownloadingResponses?: boolean onToggleEnabled?: () => void onEdit?: () => void + onViewResponses?: () => void onDownloadResponses?: () => void }) => { const [showPreview, setShowPreview] = useState(false) @@ -52,6 +54,7 @@ const GuestbookActionButtonsTestWrapper = ({ onView={() => setShowPreview(true)} onToggleEnabled={onToggleEnabled} onEdit={onEdit} + onViewResponses={onViewResponses} canToggleEnabled={canToggleEnabled} canEdit={canEdit} isTogglingEnabled={isTogglingEnabled} @@ -101,9 +104,7 @@ describe('GuestbookActionButtons', () => { expect(buttonNames.indexOf('Delete')).to.be.greaterThan( buttonNames.indexOf('Download responses') ) - expect(buttonNames.indexOf('Delete')).to.be.lessThan( - buttonNames.indexOf('View Responses') - ) + expect(buttonNames.indexOf('Delete')).to.be.lessThan(buttonNames.indexOf('View Responses')) }) }) @@ -111,7 +112,7 @@ describe('GuestbookActionButtons', () => { cy.customMount() cy.findByRole('button', { name: 'Disable' }).should('not.exist') - cy.findByRole('button', { name: 'View' }).should('exist') + cy.findByRole('button', { name: 'Preview' }).should('exist') cy.findByRole('button', { name: 'Copy' }).should('exist') cy.findByRole('button', { name: 'Edit' }).should('exist') cy.findByRole('button', { name: 'Download responses' }).should('exist') @@ -121,17 +122,17 @@ describe('GuestbookActionButtons', () => { it('does not render the edit button when editing is not allowed', () => { cy.customMount() - cy.findByRole('button', { name: 'View' }).should('exist') + cy.findByRole('button', { name: 'Preview' }).should('exist') cy.findByRole('button', { name: 'Copy' }).should('exist') cy.findByRole('button', { name: 'Edit' }).should('not.exist') cy.findByRole('button', { name: 'Download responses' }).should('exist') cy.findByRole('button', { name: 'View Responses' }).should('exist') }) - it('opens and closes the preview guestbook modal from the view button', () => { + it('opens and closes the preview guestbook modal from the preview button', () => { cy.customMount() - cy.findByRole('button', { name: 'View' }).click() + cy.findByRole('button', { name: 'Preview' }).click() cy.findByRole('dialog').should('be.visible') cy.findByText('Preview Guestbook').should('exist') cy.findByText('Downloadable Guestbook').should('exist') @@ -158,6 +159,15 @@ describe('GuestbookActionButtons', () => { cy.get('@onEdit').should('have.been.calledOnce') }) + it('triggers view responses handler when supplied', () => { + const onViewResponses = cy.stub().as('onViewResponses') + + cy.customMount() + + cy.findByRole('button', { name: 'View Responses' }).click() + cy.get('@onViewResponses').should('have.been.calledOnce') + }) + it('uses enabled button defaults when optional loading flags are omitted', () => { const onToggleEnabled = cy.stub().as('onToggleEnabled') const onDownloadResponses = cy.stub().as('onDownloadResponses') diff --git a/tests/component/sections/guestbooks/ManageGuestbooks.spec.tsx b/tests/component/sections/guestbooks/ManageGuestbooks.spec.tsx index 8b6791138..9eb6740ba 100644 --- a/tests/component/sections/guestbooks/ManageGuestbooks.spec.tsx +++ b/tests/component/sections/guestbooks/ManageGuestbooks.spec.tsx @@ -1,5 +1,6 @@ import { ReactNode, Suspense } from 'react' import { useTranslation } from 'react-i18next' +import { useLocation } from 'react-router-dom' import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' import { CollectionMother } from '@tests/component/collection/domain/models/CollectionMother' import { Guestbook } from '@/guestbooks/domain/models/Guestbook' @@ -96,6 +97,12 @@ describe('ManageGuestbooks', () => { return <>{children} } + const LocationDisplay = () => { + const location = useLocation() + + return
{location.pathname}
+ } + const defaultGuestbooks = [ guestbook, rootGuestbook, @@ -164,6 +171,7 @@ describe('ManageGuestbooks', () => { + @@ -194,6 +202,16 @@ describe('ManageGuestbooks', () => { cy.findByText('Your download has started.').should('exist') }) + it('navigates to the guestbook responses page from the view responses action', () => { + mountComponent() + + cy.contains('tbody tr', 'Downloadable Guestbook') + .findByRole('button', { name: 'View Responses' }) + .click() + + cy.findByTestId('location-display').should('have.text', '/17/guestbooks/10/responses') + }) + it('sorts guestbooks by name and toggles sort direction on repeated clicks', () => { mountComponent() @@ -466,7 +484,7 @@ describe('ManageGuestbooks', () => { it('opens and closes the preview guestbook modal from the page ui', () => { mountComponent() - cy.findAllByRole('button', { name: 'View' }).first().click() + cy.findAllByRole('button', { name: 'Preview' }).first().click() cy.findByRole('dialog').should('be.visible') cy.findByText('Preview Guestbook').should('exist') cy.findByText('Close').click() @@ -535,14 +553,14 @@ describe('ManageGuestbooks', () => { cy.findByText('The guestbook status has been updated.').should('exist') }) - it('only shows view, copy, download, and view responses actions for inherited guestbooks from a parent collection', () => { + it('only shows preview, copy, download, and view responses actions for inherited guestbooks from a parent collection', () => { mountComponent() cy.contains('tbody tr', 'Alpha Root Guestbook').within(() => { cy.findByText('Guestbook created at root').should('exist') cy.findByRole('button', { name: 'Disable' }).should('not.exist') cy.findByRole('button', { name: 'Enable' }).should('not.exist') - cy.findByRole('button', { name: 'View' }).should('exist') + cy.findByRole('button', { name: 'Preview' }).should('exist') cy.findByRole('button', { name: 'Copy' }).should('exist') cy.findByRole('button', { name: 'Edit' }).should('not.exist') cy.findByRole('button', { name: 'Download responses' }).should('exist') diff --git a/tests/component/sections/guestbooks/view-responses/GuestbookResponses.spec.tsx b/tests/component/sections/guestbooks/view-responses/GuestbookResponses.spec.tsx new file mode 100644 index 000000000..b53ff3805 --- /dev/null +++ b/tests/component/sections/guestbooks/view-responses/GuestbookResponses.spec.tsx @@ -0,0 +1,221 @@ +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { Guestbook } from '@/guestbooks/domain/models/Guestbook' +import { EventType, GuestbookResponse } from '@/guestbooks/domain/models/GuestbookResponse' +import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' +import { GuestbookRepositoryProvider } from '@/sections/guestbooks/GuestbookRepositoryProvider' +import { GuestbookResponses } from '@/sections/guestbooks/view-responses/GuestbookResponses' +import { CollectionMother } from '@tests/component/collection/domain/models/CollectionMother' + +const guestbook: Guestbook = { + id: 10, + name: 'Research Guestbook', + enabled: true, + emailRequired: true, + nameRequired: false, + institutionRequired: false, + positionRequired: false, + customQuestions: [], + createTime: '2026-01-01T00:00:00.000Z', + dataverseId: 17 +} + +const guestbookResponses: GuestbookResponse[] = [ + { + id: 1, + dataset: 'Zeta Dataset', + datasetPid: 'doi:10.5072/FK2/ZETA', + date: '2026-02-01T00:00:00.000Z', + type: EventType.DOWNLOAD, + fileName: 'zeta.csv', + fileId: 101, + userName: 'Jane User', + customQuestions: [{ question: 'Purpose', response: 'Replication' }] + }, + { + id: 2, + dataset: 'Alpha Dataset', + datasetPid: 'doi:10.5072/FK2/ALPHA', + date: '2026-01-01T00:00:00.000Z', + type: EventType.ACCESS_REQUEST, + fileName: 'alpha.tab', + fileId: 102, + userName: 'Ada User' + } +] + +const createGuestbookResponse = (id: number): GuestbookResponse => ({ + id, + dataset: `Dataset ${id.toString().padStart(2, '0')}`, + datasetPid: `doi:10.5072/FK2/${id}`, + date: `2026-01-${id.toString().padStart(2, '0')}T00:00:00.000Z`, + type: EventType.DOWNLOAD, + fileName: `file-${id}.csv`, + fileId: id, + userName: `User ${id}`, + customQuestions: [{ question: `Question ${id}`, response: `Answer ${id}` }] +}) + +describe('GuestbookResponses', () => { + const collectionRepository = {} as CollectionRepository + let guestbookRepository: GuestbookRepository + + beforeEach(() => { + collectionRepository.getById = cy.stub().resolves( + CollectionMother.create({ + id: '17', + name: 'SubCollection' + }) + ) + guestbookRepository = { + createGuestbook: cy.stub(), + editGuestbook: cy.stub(), + getGuestbook: cy.stub().as('getGuestbook').resolves(guestbook), + getGuestbooksByCollectionId: cy.stub(), + getGuestbookResponsesByGuestbookId: cy + .stub() + .as('getGuestbookResponsesByGuestbookId') + .resolves({ + guestbookResponses, + totalGuestbookResponseCount: guestbookResponses.length + }), + setGuestbookEnabled: cy.stub(), + downloadGuestbookResponsesByCollectionId: cy.stub(), + downloadGuestbookResponsesByGuestbookId: cy + .stub() + .as('downloadGuestbookResponsesByGuestbookId') + .resolves('dataset,user\nResearch Guestbook,Jane User'), + assignDatasetGuestbook: cy.stub(), + removeDatasetGuestbook: cy.stub() + } + + cy.window().then((win) => { + cy.stub(win.URL, 'createObjectURL').returns('blob:guestbook-responses') + cy.stub(win.URL, 'revokeObjectURL') + }) + }) + + const mountComponent = () => + cy.customMount( + + + + ) + + it('renders guestbook response page details and rows', () => { + mountComponent() + + cy.findAllByText('Guestbook Responses').should('have.length.at.least', 1) + cy.findByRole('heading', { name: 'SubCollection' }).should('exist') + cy.findByText(/Click "Download Responses" to download all collected responses/i).should('exist') + cy.findByText('Guestbook Name').should('exist') + cy.findByText('Research Guestbook').should('exist') + cy.findByText('2 Responses').should('exist') + cy.findByRole('button', { name: 'Download Responses' }).should('exist') + + cy.findByRole('columnheader', { name: /Dataset/ }).should('exist') + cy.findByRole('columnheader', { name: /Date/ }).should('exist') + cy.findByRole('columnheader', { name: /Type/ }).should('exist') + cy.findByRole('columnheader', { name: /File/ }).should('exist') + cy.findByRole('columnheader', { name: /User/ }).should('exist') + cy.findByRole('columnheader', { name: /Custom Questions/ }).should('exist') + cy.findByText('Zeta Dataset').should('exist') + cy.findByText('Download').should('exist') + cy.findByText('zeta.csv').should('exist') + cy.findByText('Jane User').should('exist') + cy.findByText(/Purpose: Replication/).should('exist') + + cy.get('@getGuestbook').should('have.been.calledOnceWith', guestbook.id) + cy.get('@getGuestbookResponsesByGuestbookId').should( + 'have.been.calledOnceWith', + guestbook.id, + 10, + 0 + ) + }) + + it('sorts responses by dataset', () => { + mountComponent() + + cy.get('tbody tr td:first-child').then(($cells) => { + expect([...$cells].map((cell) => cell.textContent?.trim())).to.deep.equal([ + 'Zeta Dataset', + 'Alpha Dataset' + ]) + }) + + cy.findByRole('button', { name: /^Dataset/ }).click() + + cy.get('tbody tr td:first-child').then(($cells) => { + expect([...$cells].map((cell) => cell.textContent?.trim())).to.deep.equal([ + 'Alpha Dataset', + 'Zeta Dataset' + ]) + }) + }) + + it('downloads guestbook responses from the page action', () => { + mountComponent() + + cy.findByRole('button', { name: 'Download Responses' }).click() + + cy.get('@downloadGuestbookResponsesByGuestbookId').should( + 'have.been.calledOnceWith', + guestbook.dataverseId, + guestbook.id + ) + cy.findByText('Your download has started.').should('exist') + }) + + it('shows no records found when the guestbook has no responses', () => { + ;( + guestbookRepository.getGuestbookResponsesByGuestbookId as Cypress.Agent + ).resolves({ + guestbookResponses: [], + totalGuestbookResponseCount: 0 + }) + + mountComponent() + + cy.findByText('0 Responses').should('exist') + cy.findByText('No records found.').should('exist') + }) + + it('paginates guestbook responses', () => { + const paginatedGuestbookResponses = Array.from({ length: 12 }, (_, index) => + createGuestbookResponse(index + 1) + ) + ;( + guestbookRepository.getGuestbookResponsesByGuestbookId as Cypress.Agent + ).callsFake((_guestbookId: number, limit?: number, offset?: number) => { + const pageSize = limit ?? 10 + const pageOffset = offset ?? 0 + + return Promise.resolve({ + guestbookResponses: paginatedGuestbookResponses.slice(pageOffset, pageOffset + pageSize), + totalGuestbookResponseCount: paginatedGuestbookResponses.length + }) + }) + + mountComponent() + + cy.findByText('Dataset 01').should('exist') + cy.findByText('Dataset 11').should('not.exist') + cy.findByTestId('pagination-controls').should('exist') + + cy.findByRole('button', { name: '2' }).click() + + cy.findByText('Dataset 11').should('exist') + cy.findByText(/Question 11: Answer 11/).should('exist') + cy.findByText('Dataset 01').should('not.exist') + cy.get('@getGuestbookResponsesByGuestbookId').should( + 'have.been.calledWith', + guestbook.id, + 10, + 10 + ) + }) +}) diff --git a/tests/component/sections/templates/DatasetTemplates.spec.tsx b/tests/component/sections/templates/DatasetTemplates.spec.tsx index 47b9befbd..2e8248d82 100644 --- a/tests/component/sections/templates/DatasetTemplates.spec.tsx +++ b/tests/component/sections/templates/DatasetTemplates.spec.tsx @@ -579,7 +579,7 @@ describe('Dataset Templates', () => { mountDatasetTemplates() - cy.findByRole('button', { name: 'View' }).click({ force: true }) + cy.findByRole('button', { name: 'Preview' }).click({ force: true }) cy.findByRole('dialog').within(() => { cy.findByText('Dataset Template Preview').should('exist') cy.findByText('No citation metadata is available for this template.').should('exist') @@ -603,7 +603,7 @@ describe('Dataset Templates', () => { mountDatasetTemplates() - cy.findByRole('button', { name: 'View' }).click({ force: true }) + cy.findByRole('button', { name: 'Preview' }).click({ force: true }) cy.findByRole('dialog').within(() => { cy.findByText('Test Title').should('exist') cy.findByText('No citation metadata is available for this template.').should('not.exist') @@ -635,7 +635,7 @@ describe('Dataset Templates', () => { mountDatasetTemplates() - cy.findByRole('button', { name: 'View' }).click({ force: true }) + cy.findByRole('button', { name: 'Preview' }).click({ force: true }) cy.findByRole('dialog').within(() => { cy.findByText('Title') .closest('.row') @@ -662,7 +662,7 @@ describe('Dataset Templates', () => { mountDatasetTemplates() - cy.findByRole('button', { name: 'View' }).click() + cy.findByRole('button', { name: 'Preview' }).click() cy.findByText('Close').click() cy.findByRole('dialog').should('not.exist') }) @@ -684,7 +684,7 @@ describe('Dataset Templates', () => { mountDatasetTemplates() - cy.findByRole('button', { name: 'View' }).click({ force: true }) + cy.findByRole('button', { name: 'Preview' }).click({ force: true }) cy.findByRole('dialog').within(() => { cy.findByTestId('preview-modal-skeleton').should('exist') }) @@ -699,7 +699,7 @@ describe('Dataset Templates', () => { mountDatasetTemplates() - cy.findByRole('button', { name: 'View' }).click({ force: true }) + cy.findByRole('button', { name: 'Preview' }).click({ force: true }) cy.findByRole('dialog').within(() => { cy.findByText(/Something went wrong getting the template. Try again later./i).should( 'exist' diff --git a/tests/e2e-integration/e2e/sections/guestbooks/ManageGuestbooks.spec.ts b/tests/e2e-integration/e2e/sections/guestbooks/ManageGuestbooks.spec.ts index f7d6b393b..afbfd666b 100644 --- a/tests/e2e-integration/e2e/sections/guestbooks/ManageGuestbooks.spec.ts +++ b/tests/e2e-integration/e2e/sections/guestbooks/ManageGuestbooks.spec.ts @@ -36,7 +36,7 @@ describe('Manage Guestbooks', () => { cy.contains('tbody tr', guestbookName).within(() => { cy.findByRole('button', { name: 'Disable' }).should('exist') - cy.findByRole('button', { name: 'View' }).click() + cy.findByRole('button', { name: 'Preview' }).click() }) cy.findByRole('dialog').within(() => { @@ -66,20 +66,28 @@ describe('Manage Guestbooks', () => { cy.findByRole('button', { name: 'Create Dataset Guestbook' }).should('exist') }) - it('opens the not implemented modal from copy and view responses actions', () => { + it('opens the not implemented modal from copy and navigates to view responses', () => { const guestbookName = `E2E Manage Guestbook Actions ${Date.now()}` - cy.wrap(GuestbookHelper.createAndGetByName(guestbookName), { timeout: 10000 }).then(() => { - cy.visit(GUESTBOOKS_PAGE_URL) + cy.wrap(GuestbookHelper.createAndGetByName(guestbookName), { timeout: 10000 }).then( + (guestbook) => { + cy.visit(GUESTBOOKS_PAGE_URL) - cy.contains('tbody tr', guestbookName).findByRole('button', { name: 'Copy' }).click() - closeNotImplementedModal() + cy.contains('tbody tr', guestbookName).findByRole('button', { name: 'Copy' }).click() + closeNotImplementedModal() - cy.contains('tbody tr', guestbookName) - .findByRole('button', { name: 'View Responses' }) - .click() - closeNotImplementedModal() - }) + cy.contains('tbody tr', guestbookName) + .findByRole('button', { name: 'View Responses' }) + .click() + + cy.url().should( + 'include', + `${FRONTEND_BASE_PATH}/root/guestbooks/${guestbook.id}/responses` + ) + cy.findByText('Guestbook Responses').should('exist') + cy.findByText(guestbookName).should('exist') + } + ) }) it('starts a guestbook responses download from the row download action', () => { From e0c57c56203cb6108a85eefed6dab05d4d709309 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 25 Jun 2026 12:55:23 -0400 Subject: [PATCH 07/15] fix: increase test coverage --- .../CreateGuestbook.spec.tsx | 45 +++++ .../edit-guestbook/EditGuestbook.spec.tsx | 41 +++++ .../GuestbookResponses.spec.tsx | 120 +++++++++++++ ...etGuestbookResponsesByGuestbookId.spec.tsx | 170 ++++++++++++++++++ 4 files changed, 376 insertions(+) create mode 100644 tests/component/sections/guestbooks/view-responses/useGetGuestbookResponsesByGuestbookId.spec.tsx diff --git a/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx b/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx index 8080e33e6..3851d8d11 100644 --- a/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx +++ b/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx @@ -1,6 +1,8 @@ import { act, renderHook } from '@testing-library/react' import { WriteError } from '@iqss/dataverse-client-javascript' +import { useLocation } from 'react-router-dom' import { CreateGuestbook } from '@/sections/guestbooks/create-guestbooks/CreateGuestbook' +import { CreateGuestbookButton } from '@/sections/guestbooks/create-guestbooks/CreateGuestbookButton' import { useCreateGuestbook } from '@/sections/guestbooks/create-guestbooks/useCreateGuestbook' import { GuestbookRepositoryProvider } from '@/sections/guestbooks/GuestbookRepositoryProvider' import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' @@ -159,6 +161,49 @@ describe('CreateGuestbook', () => { ] }) }) + + it('renders collection not found page when the collection cannot be fetched', () => { + collectionRepository.getById = cy.stub().rejects(new Error('missing collection')) + + mountCreateGuestbook() + + cy.findByTestId('not-found-page').should('exist') + cy.findByText(/We can't find the/i).should('exist') + cy.findByText('Collection').should('exist') + }) + + it('shows an error alert when creating the guestbook fails', () => { + createGuestbookStub.rejects(new Error('unexpected')) + + mountCreateGuestbook() + + cy.get('#guestbook-name').type('Research Use Guestbook') + cy.get('button[type="submit"]').click() + + cy.findByText(/Something went wrong creating the guestbook/i).should('exist') + }) +}) + +describe('CreateGuestbookButton', () => { + const LocationDisplay = () => { + const location = useLocation() + + return
{location.pathname}
+ } + + it('navigates to the create guestbook page when clicked', () => { + cy.customMount( + <> + + + , + ['/root/guestbooks'] + ) + + cy.findByRole('button', { name: 'Create Dataset Guestbook' }).click() + + cy.findByTestId('location-display').should('have.text', '/root/guestbooks/create') + }) }) describe('useCreateGuestbook', () => { diff --git a/tests/component/sections/guestbooks/edit-guestbook/EditGuestbook.spec.tsx b/tests/component/sections/guestbooks/edit-guestbook/EditGuestbook.spec.tsx index f7e7f55a9..d5a994b3b 100644 --- a/tests/component/sections/guestbooks/edit-guestbook/EditGuestbook.spec.tsx +++ b/tests/component/sections/guestbooks/edit-guestbook/EditGuestbook.spec.tsx @@ -106,4 +106,45 @@ describe('EditGuestbook', () => { cy.findByRole('button', { name: 'Save Changes' }).should('exist') cy.findByLabelText('Add question').should('exist') }) + + it('renders collection not found page when the collection cannot be fetched', () => { + collectionRepository.getById = cy.stub().rejects(new Error('missing collection')) + + mountComponent() + + cy.findByTestId('not-found-page').should('exist') + cy.findByText(/We can't find the/i).should('exist') + cy.findByText('Collection').should('exist') + }) + + it('renders not found page when the guestbook cannot be found', () => { + ;(guestbookRepository.getGuestbook as Cypress.Agent).resolves(undefined) + + mountComponent() + + cy.findByTestId('not-found-page').should('exist') + }) + + it('shows an error alert when getting the guestbook fails', () => { + ;(guestbookRepository.getGuestbook as Cypress.Agent).rejects( + new Error('unexpected') + ) + + mountComponent() + + cy.findByText(/Something went wrong getting the guestbook/i).should('exist') + }) + + it('shows an error alert when editing the guestbook fails', () => { + ;(guestbookRepository.editGuestbook as Cypress.Agent).rejects( + new Error('unexpected') + ) + + mountComponent() + + cy.findByDisplayValue('Research Guestbook').clear().type('Updated Guestbook') + cy.findByRole('button', { name: 'Save Changes' }).click() + + cy.findByText(/unexpected/i).should('exist') + }) }) diff --git a/tests/component/sections/guestbooks/view-responses/GuestbookResponses.spec.tsx b/tests/component/sections/guestbooks/view-responses/GuestbookResponses.spec.tsx index b53ff3805..d53930581 100644 --- a/tests/component/sections/guestbooks/view-responses/GuestbookResponses.spec.tsx +++ b/tests/component/sections/guestbooks/view-responses/GuestbookResponses.spec.tsx @@ -1,4 +1,5 @@ import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { ReadError } from '@iqss/dataverse-client-javascript' import { Guestbook } from '@/guestbooks/domain/models/Guestbook' import { EventType, GuestbookResponse } from '@/guestbooks/domain/models/GuestbookResponse' import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' @@ -157,6 +158,75 @@ describe('GuestbookResponses', () => { }) }) + it('sorts responses by date, type, file, and user and toggles sort direction', () => { + mountComponent() + + cy.findByRole('button', { name: /^Date/ }).click() + cy.get('tbody tr td:first-child').then(($cells) => { + expect([...$cells].map((cell) => cell.textContent?.trim())).to.deep.equal([ + 'Alpha Dataset', + 'Zeta Dataset' + ]) + }) + + cy.findByRole('button', { name: /^Date/ }).click() + cy.get('tbody tr td:first-child').then(($cells) => { + expect([...$cells].map((cell) => cell.textContent?.trim())).to.deep.equal([ + 'Zeta Dataset', + 'Alpha Dataset' + ]) + }) + + cy.findByRole('button', { name: /^Type/ }).click() + cy.get('tbody tr td:first-child').then(($cells) => { + expect([...$cells].map((cell) => cell.textContent?.trim())).to.deep.equal([ + 'Alpha Dataset', + 'Zeta Dataset' + ]) + }) + + cy.findByRole('button', { name: /^File/ }).click() + cy.get('tbody tr td:first-child').then(($cells) => { + expect([...$cells].map((cell) => cell.textContent?.trim())).to.deep.equal([ + 'Alpha Dataset', + 'Zeta Dataset' + ]) + }) + + cy.findByRole('button', { name: /^User/ }).click() + cy.get('tbody tr td:first-child').then(($cells) => { + expect([...$cells].map((cell) => cell.textContent?.trim())).to.deep.equal([ + 'Alpha Dataset', + 'Zeta Dataset' + ]) + }) + }) + + it('renders missing file names as empty and falls back to unknown event type label', () => { + ;( + guestbookRepository.getGuestbookResponsesByGuestbookId as Cypress.Agent + ).resolves({ + guestbookResponses: [ + { + id: 3, + dataset: 'Unknown Event Dataset', + datasetPid: 'doi:10.5072/FK2/UNKNOWN', + date: '2026-03-01T00:00:00.000Z', + type: 'UnknownEvent' as EventType, + userName: 'Unknown User' + } + ], + totalGuestbookResponseCount: 1 + }) + + mountComponent() + + cy.contains('tbody tr', 'Unknown Event Dataset').within(() => { + cy.findByText('UnknownEvent').should('exist') + cy.get('td').eq(3).should('have.text', '') + }) + }) + it('downloads guestbook responses from the page action', () => { mountComponent() @@ -170,6 +240,18 @@ describe('GuestbookResponses', () => { cy.findByText('Your download has started.').should('exist') }) + it('shows an error when downloading guestbook responses fails', () => { + ;( + guestbookRepository.downloadGuestbookResponsesByGuestbookId as Cypress.Agent + ).rejects(new Error('download failed')) + + mountComponent() + + cy.findByRole('button', { name: 'Download Responses' }).click() + + cy.findByText(/Something went wrong downloading guestbook responses/i).should('exist') + }) + it('shows no records found when the guestbook has no responses', () => { ;( guestbookRepository.getGuestbookResponsesByGuestbookId as Cypress.Agent @@ -184,6 +266,44 @@ describe('GuestbookResponses', () => { cy.findByText('No records found.').should('exist') }) + it('renders collection not found page when the collection cannot be fetched', () => { + collectionRepository.getById = cy.stub().rejects(new Error('missing collection')) + + mountComponent() + + cy.findByTestId('not-found-page').should('exist') + cy.findByText(/We can't find the/i).should('exist') + cy.findByText('Collection').should('exist') + }) + + it('renders not found page when the guestbook cannot be found', () => { + ;(guestbookRepository.getGuestbook as Cypress.Agent).resolves(undefined) + + mountComponent() + + cy.findByTestId('not-found-page').should('exist') + }) + + it('shows an error alert when getting the guestbook fails', () => { + ;(guestbookRepository.getGuestbook as Cypress.Agent).rejects( + new ReadError('Guestbook lookup failed') + ) + + mountComponent() + + cy.findByText(/Guestbook lookup failed/i).should('exist') + }) + + it('shows an error alert when getting guestbook responses fails', () => { + ;( + guestbookRepository.getGuestbookResponsesByGuestbookId as Cypress.Agent + ).rejects(new Error('unexpected')) + + mountComponent() + + cy.findByText(/Something went wrong getting guestbook responses/i).should('exist') + }) + it('paginates guestbook responses', () => { const paginatedGuestbookResponses = Array.from({ length: 12 }, (_, index) => createGuestbookResponse(index + 1) diff --git a/tests/component/sections/guestbooks/view-responses/useGetGuestbookResponsesByGuestbookId.spec.tsx b/tests/component/sections/guestbooks/view-responses/useGetGuestbookResponsesByGuestbookId.spec.tsx new file mode 100644 index 000000000..ca0079c35 --- /dev/null +++ b/tests/component/sections/guestbooks/view-responses/useGetGuestbookResponsesByGuestbookId.spec.tsx @@ -0,0 +1,170 @@ +import { act, renderHook, waitFor } from '@testing-library/react' +import { ReadError } from '@iqss/dataverse-client-javascript' +import { EventType, GuestbookResponse } from '@/guestbooks/domain/models/GuestbookResponse' +import { GuestbookResponseSubset } from '@/guestbooks/domain/models/GuestbookResponse' +import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' +import { useGetGuestbookResponsesByGuestbookId } from '@/sections/guestbooks/view-responses/useGetGuestbookResponsesByGuestbookId' +import { createGuestbookRepositoryStub } from '../createGuestbookRepositoryStub' + +const guestbookResponse: GuestbookResponse = { + id: 1, + dataset: 'Response Dataset', + datasetPid: 'doi:10.5072/FK2/RESP', + date: '2026-01-01T00:00:00.000Z', + type: EventType.DOWNLOAD, + email: 'user@example.com', + fileId: 101, + fileName: 'response.csv', + userName: 'Guest', + customQuestions: [{ question: 'Purpose', response: 'Research' }] +} + +describe('useGetGuestbookResponsesByGuestbookId', () => { + let guestbookRepository: GuestbookRepository + + beforeEach(() => { + guestbookRepository = createGuestbookRepositoryStub() + }) + + it('returns guestbook responses when request succeeds', async () => { + const getGuestbookResponsesByGuestbookIdStub = + guestbookRepository.getGuestbookResponsesByGuestbookId as Cypress.Agent + getGuestbookResponsesByGuestbookIdStub.resolves({ + guestbookResponses: [guestbookResponse], + totalGuestbookResponseCount: 1 + }) + + const { result } = renderHook(() => + useGetGuestbookResponsesByGuestbookId({ + guestbookRepository, + guestbookId: 10, + limit: 10, + offset: 0 + }) + ) + + await waitFor(() => { + expect(result.current.isLoadingGuestbookResponses).to.deep.equal(false) + expect(result.current.errorGetGuestbookResponses).to.deep.equal(null) + expect(result.current.guestbookResponses).to.deep.equal([guestbookResponse]) + expect(result.current.totalGuestbookResponseCount).to.deep.equal(1) + }) + + cy.wrap(getGuestbookResponsesByGuestbookIdStub).should('have.been.calledOnceWith', 10, 10, 0) + }) + + it('does not fetch when guestbook id is undefined', async () => { + const getGuestbookResponsesByGuestbookIdStub = + guestbookRepository.getGuestbookResponsesByGuestbookId as Cypress.Agent + getGuestbookResponsesByGuestbookIdStub.resolves({ + guestbookResponses: [guestbookResponse], + totalGuestbookResponseCount: 1 + }) + + const { result } = renderHook(() => + useGetGuestbookResponsesByGuestbookId({ + guestbookRepository, + guestbookId: undefined + }) + ) + + await waitFor(() => { + expect(result.current.isLoadingGuestbookResponses).to.deep.equal(false) + expect(result.current.errorGetGuestbookResponses).to.deep.equal(null) + expect(result.current.guestbookResponses).to.deep.equal([]) + expect(result.current.totalGuestbookResponseCount).to.deep.equal(0) + }) + + cy.wrap(getGuestbookResponsesByGuestbookIdStub).should('not.have.been.called') + }) + + it('uses empty defaults when response fields are missing', async () => { + const getGuestbookResponsesByGuestbookIdStub = + guestbookRepository.getGuestbookResponsesByGuestbookId as Cypress.Agent + getGuestbookResponsesByGuestbookIdStub.resolves({} as GuestbookResponseSubset) + + const { result } = renderHook(() => + useGetGuestbookResponsesByGuestbookId({ + guestbookRepository, + guestbookId: 10, + limit: 5, + offset: 15 + }) + ) + + await waitFor(() => { + expect(result.current.isLoadingGuestbookResponses).to.deep.equal(false) + expect(result.current.errorGetGuestbookResponses).to.deep.equal(null) + expect(result.current.guestbookResponses).to.deep.equal([]) + expect(result.current.totalGuestbookResponseCount).to.deep.equal(0) + }) + + cy.wrap(getGuestbookResponsesByGuestbookIdStub).should('have.been.calledOnceWith', 10, 5, 15) + }) + + it('resets responses and sets formatted message when request fails with ReadError', async () => { + const getGuestbookResponsesByGuestbookIdStub = + guestbookRepository.getGuestbookResponsesByGuestbookId as Cypress.Agent + getGuestbookResponsesByGuestbookIdStub.onFirstCall().resolves({ + guestbookResponses: [guestbookResponse], + totalGuestbookResponseCount: 1 + }) + getGuestbookResponsesByGuestbookIdStub + .onSecondCall() + .rejects(new ReadError('ReadError message')) + + const { result } = renderHook(() => + useGetGuestbookResponsesByGuestbookId({ + guestbookRepository, + guestbookId: 10 + }) + ) + + await waitFor(() => { + expect(result.current.guestbookResponses).to.deep.equal([guestbookResponse]) + expect(result.current.totalGuestbookResponseCount).to.deep.equal(1) + }) + + await act(async () => { + await result.current.fetchGuestbookResponses() + }) + + expect(result.current.isLoadingGuestbookResponses).to.deep.equal(false) + expect(result.current.guestbookResponses).to.deep.equal([]) + expect(result.current.totalGuestbookResponseCount).to.deep.equal(0) + expect(result.current.errorGetGuestbookResponses).to.deep.equal('ReadError message') + }) + + it('resets responses and sets default message when request fails with non-ReadError', async () => { + const getGuestbookResponsesByGuestbookIdStub = + guestbookRepository.getGuestbookResponsesByGuestbookId as Cypress.Agent + getGuestbookResponsesByGuestbookIdStub.onFirstCall().resolves({ + guestbookResponses: [guestbookResponse], + totalGuestbookResponseCount: 1 + }) + getGuestbookResponsesByGuestbookIdStub.onSecondCall().rejects(new Error('unexpected')) + + const { result } = renderHook(() => + useGetGuestbookResponsesByGuestbookId({ + guestbookRepository, + guestbookId: 10 + }) + ) + + await waitFor(() => { + expect(result.current.guestbookResponses).to.deep.equal([guestbookResponse]) + expect(result.current.totalGuestbookResponseCount).to.deep.equal(1) + }) + + await act(async () => { + await result.current.fetchGuestbookResponses() + }) + + expect(result.current.isLoadingGuestbookResponses).to.deep.equal(false) + expect(result.current.guestbookResponses).to.deep.equal([]) + expect(result.current.totalGuestbookResponseCount).to.deep.equal(0) + expect(result.current.errorGetGuestbookResponses).to.deep.equal( + 'Something went wrong getting guestbook responses. Try again later.' + ) + }) +}) From ca340f6a8631057571ad00906f75c9ab306c6e63 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 25 Jun 2026 14:01:39 -0400 Subject: [PATCH 08/15] fix: e2e test error --- .../guestbooks/ManageGuestbooks.spec.ts | 24 ------------------- .../templates/DatasetTemplates.spec.tsx | 2 +- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/tests/e2e-integration/e2e/sections/guestbooks/ManageGuestbooks.spec.ts b/tests/e2e-integration/e2e/sections/guestbooks/ManageGuestbooks.spec.ts index afbfd666b..2befe6409 100644 --- a/tests/e2e-integration/e2e/sections/guestbooks/ManageGuestbooks.spec.ts +++ b/tests/e2e-integration/e2e/sections/guestbooks/ManageGuestbooks.spec.ts @@ -66,30 +66,6 @@ describe('Manage Guestbooks', () => { cy.findByRole('button', { name: 'Create Dataset Guestbook' }).should('exist') }) - it('opens the not implemented modal from copy and navigates to view responses', () => { - const guestbookName = `E2E Manage Guestbook Actions ${Date.now()}` - - cy.wrap(GuestbookHelper.createAndGetByName(guestbookName), { timeout: 10000 }).then( - (guestbook) => { - cy.visit(GUESTBOOKS_PAGE_URL) - - cy.contains('tbody tr', guestbookName).findByRole('button', { name: 'Copy' }).click() - closeNotImplementedModal() - - cy.contains('tbody tr', guestbookName) - .findByRole('button', { name: 'View Responses' }) - .click() - - cy.url().should( - 'include', - `${FRONTEND_BASE_PATH}/root/guestbooks/${guestbook.id}/responses` - ) - cy.findByText('Guestbook Responses').should('exist') - cy.findByText(guestbookName).should('exist') - } - ) - }) - it('starts a guestbook responses download from the row download action', () => { const guestbookName = `E2E Manage Guestbook Download ${Date.now()}` diff --git a/tests/e2e-integration/e2e/sections/templates/DatasetTemplates.spec.tsx b/tests/e2e-integration/e2e/sections/templates/DatasetTemplates.spec.tsx index 694ef000f..17935d506 100644 --- a/tests/e2e-integration/e2e/sections/templates/DatasetTemplates.spec.tsx +++ b/tests/e2e-integration/e2e/sections/templates/DatasetTemplates.spec.tsx @@ -68,7 +68,7 @@ describe('Dataset Templates', () => { cy.findByText(templateName) .closest('tr') .within(() => { - cy.findByRole('button', { name: 'View' }).click({ force: true }) + cy.findByRole('button', { name: 'Preview' }).click({ force: true }) }) cy.findByText(/Dataset Template Preview/i).should('exist') From cc8dd2f2bc6c772ef5f2804f53df91e7fca4b6d0 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 26 Jun 2026 14:12:45 -0400 Subject: [PATCH 09/15] fix: make guestbooks enabled by default --- src/sections/guestbooks/guestbook-form/GuestbookForm.tsx | 2 +- .../guestbooks/create-guestbooks/CreateGuestbook.spec.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sections/guestbooks/guestbook-form/GuestbookForm.tsx b/src/sections/guestbooks/guestbook-form/GuestbookForm.tsx index 84d32b695..f54d8091b 100644 --- a/src/sections/guestbooks/guestbook-form/GuestbookForm.tsx +++ b/src/sections/guestbooks/guestbook-form/GuestbookForm.tsx @@ -148,7 +148,7 @@ export const GuestbookForm = ({ const buildGuestbookDTO = (): GuestbookDTO => ({ ...(initialGuestbook?.id !== undefined ? { id: initialGuestbook.id } : {}), name: guestbookName.trim(), - enabled: initialGuestbook?.enabled ?? false, + enabled: initialGuestbook?.enabled ?? true, nameRequired, emailRequired, institutionRequired, diff --git a/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx b/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx index 3851d8d11..f1b1e5881 100644 --- a/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx +++ b/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx @@ -85,7 +85,7 @@ describe('CreateGuestbook', () => { expectGuestbookCreatedWith({ name: 'Research Use Guestbook', - enabled: false, + enabled: true, nameRequired: false, emailRequired: false, institutionRequired: false, @@ -133,7 +133,7 @@ describe('CreateGuestbook', () => { expectGuestbookCreatedWith({ name: 'Research Use Guestbook', - enabled: false, + enabled: true, nameRequired: true, emailRequired: false, institutionRequired: true, From 260297df238d82f1ac25c3774a3ad567ba307483 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 26 Jun 2026 15:25:18 -0400 Subject: [PATCH 10/15] feat: copy guestbook action button --- src/sections/guestbooks/ManageGuestbooks.tsx | 5 ++ .../action-buttons/GuestbookActionButtons.tsx | 16 ++-- .../create-guestbooks/CreateGuestbook.tsx | 37 +++++++- .../guestbook-form/GuestbookForm.tsx | 6 +- .../GuestbookActionButtons.spec.tsx | 30 +++---- .../guestbooks/ManageGuestbooks.spec.tsx | 8 ++ .../CreateGuestbook.spec.tsx | 87 +++++++++++++++++++ 7 files changed, 155 insertions(+), 34 deletions(-) diff --git a/src/sections/guestbooks/ManageGuestbooks.tsx b/src/sections/guestbooks/ManageGuestbooks.tsx index 3061badc6..539d9059a 100644 --- a/src/sections/guestbooks/ManageGuestbooks.tsx +++ b/src/sections/guestbooks/ManageGuestbooks.tsx @@ -377,6 +377,11 @@ export const Guestbooks = ({ collectionRepository, collectionId }: GuestbooksPro setGuestbookToPreview(guestbook)} + onCopy={() => + navigate(RouteWithParams.GUESTBOOKS_CREATE(collectionId), { + state: { guestbookToCopy: guestbook } + }) + } onEdit={() => navigate(RouteWithParams.GUESTBOOKS_EDIT(collectionId, guestbook.id)) } diff --git a/src/sections/guestbooks/action-buttons/GuestbookActionButtons.tsx b/src/sections/guestbooks/action-buttons/GuestbookActionButtons.tsx index 8d8d27680..c9ef7a799 100644 --- a/src/sections/guestbooks/action-buttons/GuestbookActionButtons.tsx +++ b/src/sections/guestbooks/action-buttons/GuestbookActionButtons.tsx @@ -8,6 +8,7 @@ interface GuestbookActionButtonsProps { isEnabled: boolean onView: () => void onToggleEnabled: () => void + onCopy?: () => void onEdit?: () => void onViewResponses?: () => void canToggleEnabled?: boolean @@ -23,6 +24,7 @@ export const GuestbookActionButtons = ({ isEnabled, onView, onToggleEnabled, + onCopy, onEdit, onViewResponses, canToggleEnabled = true, @@ -55,21 +57,13 @@ export const GuestbookActionButtons = ({ - {canEdit && ( - @@ -99,7 +93,7 @@ export const GuestbookActionButtons = ({ diff --git a/src/sections/guestbooks/create-guestbooks/CreateGuestbook.tsx b/src/sections/guestbooks/create-guestbooks/CreateGuestbook.tsx index fed930802..43b916147 100644 --- a/src/sections/guestbooks/create-guestbooks/CreateGuestbook.tsx +++ b/src/sections/guestbooks/create-guestbooks/CreateGuestbook.tsx @@ -1,8 +1,9 @@ -import { useEffect } from 'react' +import { useEffect, useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' import { Alert } from '@iqss/dataverse-design-system' -import { type NavigateFunction, useNavigate } from 'react-router-dom' +import { type NavigateFunction, useLocation, useNavigate } from 'react-router-dom' import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { Guestbook } from '@/guestbooks/domain/models/Guestbook' import { GuestbookDTO } from '@/guestbooks/domain/useCases/DTOs/GuestbookDTO' import { RouteWithParams } from '@/sections/Route.enum' import { useCollection } from '@/sections/collection/useCollection' @@ -19,12 +20,18 @@ interface CreateGuestbookProps { collectionRepository: CollectionRepository } +interface CreateGuestbookLocationState { + guestbookToCopy?: Guestbook +} + export const CreateGuestbook = ({ collectionId, collectionRepository }: CreateGuestbookProps) => { const { t } = useTranslation('guestbooks') const navigate: NavigateFunction = useNavigate() + const location = useLocation() const guestbookRepository = useGuestbookRepository() const { setIsLoading } = useLoading() const { collection, isLoading } = useCollection(collectionRepository, collectionId) + const guestbookToCopy = (location.state as CreateGuestbookLocationState | null)?.guestbookToCopy const guestbooksGuideUrl = 'https://guides.dataverse.org/en/latest/user/dataverse-management.html#dataset-guestbooks' const navigateToGuestbooks = () => navigate(RouteWithParams.GUESTBOOKS(collectionId)) @@ -35,6 +42,31 @@ export const CreateGuestbook = ({ collectionId, collectionRepository }: CreateGu onSuccessfulCreate: navigateToGuestbooks } ) + const initialGuestbook = useMemo( + () => + guestbookToCopy + ? { + name: `Copy of ${guestbookToCopy.name}`, + enabled: guestbookToCopy.enabled, + emailRequired: guestbookToCopy.emailRequired, + nameRequired: guestbookToCopy.nameRequired, + institutionRequired: guestbookToCopy.institutionRequired, + positionRequired: guestbookToCopy.positionRequired, + customQuestions: guestbookToCopy.customQuestions.map((question, index) => ({ + question: question.question, + required: question.required, + displayOrder: index, + type: question.type, + hidden: question.hidden, + optionValues: question.optionValues?.map((option, optionIndex) => ({ + value: option.value, + displayOrder: optionIndex + })) + })) + } + : undefined, + [guestbookToCopy] + ) useEffect(() => { setIsLoading(isLoading) @@ -82,6 +114,7 @@ export const CreateGuestbook = ({ collectionId, collectionRepository }: CreateGu {errorCreatingGuestbook && {errorCreatingGuestbook}} ({ responseOptions: [{ value: '' }] }) -const getInitialQuestions = (guestbook?: Guestbook): CustomQuestionDraft[] => { +const getInitialQuestions = (guestbook?: GuestbookDTO): CustomQuestionDraft[] => { const customQuestions = guestbook?.customQuestions ?? [] if (customQuestions.length === 0) { diff --git a/tests/component/sections/guestbooks/GuestbookActionButtons.spec.tsx b/tests/component/sections/guestbooks/GuestbookActionButtons.spec.tsx index e8c827063..8589a63d6 100644 --- a/tests/component/sections/guestbooks/GuestbookActionButtons.spec.tsx +++ b/tests/component/sections/guestbooks/GuestbookActionButtons.spec.tsx @@ -31,6 +31,7 @@ const GuestbookActionButtonsTestWrapper = ({ isTogglingEnabled = false, isDownloadingResponses = false, onToggleEnabled = () => {}, + onCopy, onEdit, onViewResponses, onDownloadResponses = () => {} @@ -41,6 +42,7 @@ const GuestbookActionButtonsTestWrapper = ({ isTogglingEnabled?: boolean isDownloadingResponses?: boolean onToggleEnabled?: () => void + onCopy?: () => void onEdit?: () => void onViewResponses?: () => void onDownloadResponses?: () => void @@ -53,6 +55,7 @@ const GuestbookActionButtonsTestWrapper = ({ isEnabled={isEnabled} onView={() => setShowPreview(true)} onToggleEnabled={onToggleEnabled} + onCopy={onCopy} onEdit={onEdit} onViewResponses={onViewResponses} canToggleEnabled={canToggleEnabled} @@ -159,6 +162,15 @@ describe('GuestbookActionButtons', () => { cy.get('@onEdit').should('have.been.calledOnce') }) + it('triggers copy handler when supplied', () => { + const onCopy = cy.stub().as('onCopy') + + cy.customMount() + + cy.findByRole('button', { name: 'Copy' }).click() + cy.get('@onCopy').should('have.been.calledOnce') + }) + it('triggers view responses handler when supplied', () => { const onViewResponses = cy.stub().as('onViewResponses') @@ -188,24 +200,6 @@ describe('GuestbookActionButtons', () => { cy.get('@onDownloadResponses').should('have.been.calledOnce') }) - it('opens the not implemented modal from copy, edit, and view responses buttons', () => { - cy.customMount() - - cy.findByRole('button', { name: 'Copy' }).click() - cy.findByText('Not Implemented').should('exist') - cy.findByText(/This feature is not implemented yet in the Modern version./i).should('exist') - cy.findByText('Close').click() - cy.findByText('Not Implemented').should('not.exist') - - cy.findByRole('button', { name: 'Edit' }).click() - cy.findByText('Not Implemented').should('exist') - cy.findByText('Close').click() - cy.findByText('Not Implemented').should('not.exist') - - cy.findByRole('button', { name: 'View Responses' }).click() - cy.findByText('Not Implemented').should('exist') - }) - it('opens the not implemented modal from the disabled guestbook delete button', () => { cy.customMount() diff --git a/tests/component/sections/guestbooks/ManageGuestbooks.spec.tsx b/tests/component/sections/guestbooks/ManageGuestbooks.spec.tsx index 9eb6740ba..30008dd4f 100644 --- a/tests/component/sections/guestbooks/ManageGuestbooks.spec.tsx +++ b/tests/component/sections/guestbooks/ManageGuestbooks.spec.tsx @@ -212,6 +212,14 @@ describe('ManageGuestbooks', () => { cy.findByTestId('location-display').should('have.text', '/17/guestbooks/10/responses') }) + it('navigates to the create guestbook page from the copy action', () => { + mountComponent() + + cy.contains('tbody tr', 'Downloadable Guestbook').findByRole('button', { name: 'Copy' }).click() + + cy.findByTestId('location-display').should('have.text', '/17/guestbooks/create') + }) + it('sorts guestbooks by name and toggles sort direction on repeated clicks', () => { mountComponent() diff --git a/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx b/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx index f1b1e5881..301312e1d 100644 --- a/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx +++ b/tests/component/sections/guestbooks/create-guestbooks/CreateGuestbook.spec.tsx @@ -6,6 +6,7 @@ import { CreateGuestbookButton } from '@/sections/guestbooks/create-guestbooks/C import { useCreateGuestbook } from '@/sections/guestbooks/create-guestbooks/useCreateGuestbook' import { GuestbookRepositoryProvider } from '@/sections/guestbooks/GuestbookRepositoryProvider' import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { Guestbook } from '@/guestbooks/domain/models/Guestbook' import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' import { GuestbookDTO } from '@/guestbooks/domain/useCases/DTOs/GuestbookDTO' import { CollectionMother } from '@tests/component/collection/domain/models/CollectionMother' @@ -34,6 +35,32 @@ const guestbook: GuestbookDTO = { ] } +const sourceGuestbookToCopy: Guestbook = { + id: 10, + name: 'Source Guestbook', + enabled: false, + emailRequired: true, + nameRequired: true, + institutionRequired: true, + positionRequired: false, + customQuestions: [ + { + id: 21, + question: 'Preferred format', + required: true, + displayOrder: 0, + type: 'options', + hidden: false, + optionValues: [ + { id: 31, value: 'CSV', displayOrder: 0 }, + { id: 32, value: 'JSON', displayOrder: 1 } + ] + } + ], + createTime: '2026-01-01T00:00:00.000Z', + dataverseId: 17 +} + describe('CreateGuestbook', () => { const collectionRepository = {} as CollectionRepository let guestbookRepository: GuestbookRepository @@ -57,6 +84,13 @@ describe('CreateGuestbook', () => { ) + const mountCopyGuestbook = () => + cy.customMount( + + + , + [{ pathname: '/root/guestbooks/create', state: { guestbookToCopy: sourceGuestbookToCopy } }] + ) const expectGuestbookCreatedWith = (expectedGuestbook: GuestbookDTO) => { cy.wrap(null).should(() => { @@ -68,6 +102,59 @@ describe('CreateGuestbook', () => { }) } + it('creates guestbooks as enabled by default', () => { + mountCreateGuestbook() + + cy.get('#guestbook-name').type('Enabled By Default Guestbook') + cy.get('button[type="submit"]').click() + + cy.wrap(null).should(() => { + expect(createGuestbookStub).to.have.been.calledOnce + expect(createGuestbookStub.getCall(0).args[1].enabled).to.equal(true) + }) + }) + + it('prefills a copied guestbook and submits it as a new guestbook', () => { + mountCopyGuestbook() + + cy.findByDisplayValue('Copy of Source Guestbook').should('exist') + cy.findByLabelText('Name').should('be.checked') + cy.findByLabelText('Email').should('be.checked') + cy.findByLabelText('Institution').should('be.checked') + cy.findByLabelText('Position').should('not.be.checked') + cy.findByDisplayValue('Preferred format').should('exist') + cy.findByDisplayValue('CSV').should('exist') + cy.findByDisplayValue('JSON').should('exist') + cy.findByLabelText('Required field').should('be.checked') + + cy.get('button[type="submit"]').click() + cy.wrap(guestbookRepository.getGuestbook as Cypress.Agent).should( + 'not.have.been.called' + ) + + expectGuestbookCreatedWith({ + name: 'Copy of Source Guestbook', + enabled: false, + nameRequired: true, + emailRequired: true, + institutionRequired: true, + positionRequired: false, + customQuestions: [ + { + question: 'Preferred format', + required: true, + displayOrder: 0, + type: 'options', + hidden: false, + optionValues: [ + { value: 'CSV', displayOrder: 0 }, + { value: 'JSON', displayOrder: 1 } + ] + } + ] + }) + }) + it('submits a guestbook with single line and multiple line custom questions', () => { mountCreateGuestbook() From 6c547a669e0720798b71f7fc954323a830f0261c Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 26 Jun 2026 16:21:56 -0400 Subject: [PATCH 11/15] fix: accessibility test error --- .../items-list/collection-card/CollectionCard.module.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sections/collection/collection-items-panel/items-list/collection-card/CollectionCard.module.scss b/src/sections/collection/collection-items-panel/items-list/collection-card/CollectionCard.module.scss index f07a3d791..20ee7a9c9 100644 --- a/src/sections/collection/collection-items-panel/items-list/collection-card/CollectionCard.module.scss +++ b/src/sections/collection/collection-items-panel/items-list/collection-card/CollectionCard.module.scss @@ -19,6 +19,8 @@ a { @include link-hover-underlined; + text-decoration: underline; + text-underline-offset: 2px; } .affiliation { @@ -80,6 +82,8 @@ a { @include link-hover-underlined; + text-decoration: underline; + text-underline-offset: 2px; } .description { From 1839bd6defff6c1ca968502b0684e8cc29e60f0e Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Fri, 26 Jun 2026 16:31:57 -0400 Subject: [PATCH 12/15] fix: Lint error --- .../items-list/collection-card/CollectionCard.module.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sections/collection/collection-items-panel/items-list/collection-card/CollectionCard.module.scss b/src/sections/collection/collection-items-panel/items-list/collection-card/CollectionCard.module.scss index 20ee7a9c9..0ae6c774b 100644 --- a/src/sections/collection/collection-items-panel/items-list/collection-card/CollectionCard.module.scss +++ b/src/sections/collection/collection-items-panel/items-list/collection-card/CollectionCard.module.scss @@ -19,6 +19,7 @@ a { @include link-hover-underlined; + text-decoration: underline; text-underline-offset: 2px; } @@ -82,6 +83,7 @@ a { @include link-hover-underlined; + text-decoration: underline; text-underline-offset: 2px; } From fd320fc3834c52652e43073be66081480a705ade Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 1 Jul 2026 09:01:32 -0400 Subject: [PATCH 13/15] fix: update guestbookDTO --- package-lock.json | 8 ++++---- package.json | 2 +- .../domain/useCases/DTOs/GuestbookDTO.ts | 3 +-- .../guestbooks/guestbook-form/GuestbookForm.tsx | 1 - .../edit-guestbook/useEditGuestbook.spec.tsx | 14 +++++++------- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 99a8b2ada..b1e8be70b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "v2.2.0-pr456.8edcbca", + "@iqss/dataverse-client-javascript": "2.2.0-alpha.11", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -1953,9 +1953,9 @@ } }, "node_modules/@iqss/dataverse-client-javascript": { - "version": "2.2.0-pr456.8edcbca", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.2.0-pr456.8edcbca/ee7f76c05ce4d1402be99a70eb9cd9e1713b72fb", - "integrity": "sha512-OW6Za/jFpHnY647bGmsYETyeX0PR661BSVTxycSGUvbKlpDer/91RtJN0qiwdwW03ZSoanR4LwDeZAoBaD9vqA==", + "version": "2.2.0-alpha.11", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.2.0-alpha.11/3a0a53022fcd9c9dd88f15815a5d1ad46b38a40a", + "integrity": "sha512-stBrA0fG2rHcNhyX8+43CeNPo8YyzZ1h5cKSOxLKAnKGBeF99FaTNhjxxzCQ5r2RlXzLw8EHZ5day0aL/0vDgA==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index 20afbf7f7..647c5dad5 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "v2.2.0-pr456.8edcbca", + "@iqss/dataverse-client-javascript": "2.2.0-alpha.11", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", diff --git a/src/guestbooks/domain/useCases/DTOs/GuestbookDTO.ts b/src/guestbooks/domain/useCases/DTOs/GuestbookDTO.ts index 9022b0572..5b7bef045 100644 --- a/src/guestbooks/domain/useCases/DTOs/GuestbookDTO.ts +++ b/src/guestbooks/domain/useCases/DTOs/GuestbookDTO.ts @@ -1,7 +1,6 @@ import { GuestbookQuestionType } from '../../models/Guestbook' export interface GuestbookDTO { - id?: number name: string enabled: boolean emailRequired: boolean @@ -9,7 +8,7 @@ export interface GuestbookDTO { institutionRequired: boolean positionRequired: boolean createTime?: string - customQuestions: GuestbookCustomQuestionDTO[] + customQuestions?: GuestbookCustomQuestionDTO[] } export interface GuestbookCustomQuestionDTO { diff --git a/src/sections/guestbooks/guestbook-form/GuestbookForm.tsx b/src/sections/guestbooks/guestbook-form/GuestbookForm.tsx index 7c7064916..035ccbecd 100644 --- a/src/sections/guestbooks/guestbook-form/GuestbookForm.tsx +++ b/src/sections/guestbooks/guestbook-form/GuestbookForm.tsx @@ -146,7 +146,6 @@ export const GuestbookForm = ({ } const buildGuestbookDTO = (): GuestbookDTO => ({ - ...(initialGuestbook?.id !== undefined ? { id: initialGuestbook.id } : {}), name: guestbookName.trim(), enabled: initialGuestbook?.enabled ?? true, nameRequired, diff --git a/tests/component/sections/guestbooks/edit-guestbook/useEditGuestbook.spec.tsx b/tests/component/sections/guestbooks/edit-guestbook/useEditGuestbook.spec.tsx index 805f13bc7..f4141e354 100644 --- a/tests/component/sections/guestbooks/edit-guestbook/useEditGuestbook.spec.tsx +++ b/tests/component/sections/guestbooks/edit-guestbook/useEditGuestbook.spec.tsx @@ -7,8 +7,8 @@ import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookR import { GuestbookDTO } from '@/guestbooks/domain/useCases/DTOs/GuestbookDTO' import { useEditGuestbook } from '@/sections/guestbooks/edit-guestbook/useEditGuestbook' +const guestbookId = 10 const guestbook: GuestbookDTO = { - id: 10, name: 'Research Guestbook', enabled: true, emailRequired: true, @@ -93,10 +93,10 @@ describe('useEditGuestbook', () => { const { result } = renderUseEditGuestbook() await act(async () => { - await result.current.handleEditGuestbook(guestbook.id as number, guestbook) + await result.current.handleEditGuestbook(guestbookId, guestbook) }) - expect(editGuestbookStub).to.have.been.calledOnceWith(guestbook.id, guestbook) + expect(editGuestbookStub).to.have.been.calledOnceWith(guestbookId, guestbook) expect(onSuccessfulEdit).to.have.been.calledOnce expect(result.current.isEditingGuestbook).to.equal(false) expect(result.current.errorEditingGuestbook).to.equal(null) @@ -110,7 +110,7 @@ describe('useEditGuestbook', () => { const { result } = renderUseEditGuestbook() await act(async () => { - await result.current.handleEditGuestbook(guestbook.id as number, guestbook) + await result.current.handleEditGuestbook(guestbookId, guestbook) }) expect(onSuccessfulEdit).not.to.have.been.called @@ -129,7 +129,7 @@ describe('useEditGuestbook', () => { const { result } = renderUseEditGuestbook() await act(async () => { - await result.current.handleEditGuestbook(guestbook.id as number, guestbook) + await result.current.handleEditGuestbook(guestbookId, guestbook) }) expect(onSuccessfulEdit).not.to.have.been.called @@ -143,7 +143,7 @@ describe('useEditGuestbook', () => { const { result } = renderUseEditGuestbook() await act(async () => { - await result.current.handleEditGuestbook(guestbook.id as number, guestbook) + await result.current.handleEditGuestbook(guestbookId, guestbook) }) expect(onSuccessfulEdit).not.to.have.been.called @@ -157,7 +157,7 @@ describe('useEditGuestbook', () => { const { result } = renderUseEditGuestbook() await act(async () => { - await result.current.handleEditGuestbook(guestbook.id as number, guestbook) + await result.current.handleEditGuestbook(guestbookId, guestbook) }) expect(onSuccessfulEdit).not.to.have.been.called From d62152e48374e7d5a8125efd039d232cbab23954 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Wed, 1 Jul 2026 13:24:38 -0400 Subject: [PATCH 14/15] fix: copilot review --- public/locales/en/guestbooks.json | 6 ++++++ public/locales/es/datasetTemplates.json | 2 +- public/locales/es/guestbooks.json | 6 ++++++ .../view-responses/GuestbookResponses.tsx | 18 +++++++----------- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/public/locales/en/guestbooks.json b/public/locales/en/guestbooks.json index 7510305c7..6fb047787 100644 --- a/public/locales/en/guestbooks.json +++ b/public/locales/en/guestbooks.json @@ -117,6 +117,12 @@ "file": "File", "user": "User", "customQuestions": "Custom Questions" + }, + "eventTypes": { + "AccessRequest": "Access Request", + "Download": "Download", + "Subset": "Subset", + "Explore": "Explore" } } } diff --git a/public/locales/es/datasetTemplates.json b/public/locales/es/datasetTemplates.json index 13bb0c65d..77367f565 100644 --- a/public/locales/es/datasetTemplates.json +++ b/public/locales/es/datasetTemplates.json @@ -8,7 +8,7 @@ "create": "Crear plantilla de conjunto de datos", "makeDefault": "Hacer predeterminada", "default": "Predeterminada", - "view": "Ver", + "view": "Vista previa", "copy": "Copiar", "edit": "Editar plantilla", "delete": "Eliminar" diff --git a/public/locales/es/guestbooks.json b/public/locales/es/guestbooks.json index cccaa144f..a53553fb3 100644 --- a/public/locales/es/guestbooks.json +++ b/public/locales/es/guestbooks.json @@ -117,6 +117,12 @@ "file": "Fichero", "user": "Usuario", "customQuestions": "Preguntas personalizadas" + }, + "eventTypes": { + "AccessRequest": "Solicitud de acceso", + "Download": "Descarga", + "Subset": "Subconjunto", + "Explore": "Explorar" } } } diff --git a/src/sections/guestbooks/view-responses/GuestbookResponses.tsx b/src/sections/guestbooks/view-responses/GuestbookResponses.tsx index a224505ba..9048fc1b0 100644 --- a/src/sections/guestbooks/view-responses/GuestbookResponses.tsx +++ b/src/sections/guestbooks/view-responses/GuestbookResponses.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { Alert, Button, Table } from '@iqss/dataverse-design-system' import { CaretDown, CaretUp, ChevronExpand, Download } from 'react-bootstrap-icons' @@ -29,13 +29,6 @@ interface GuestbookResponsesProps { type SortableColumn = 'dataset' | 'date' | 'type' | 'file' | 'user' -const eventTypeLabels: Record = { - [EventType.ACCESS_REQUEST]: 'Access Request', - [EventType.DOWNLOAD]: 'Download', - [EventType.SUBSET]: 'Subset', - [EventType.EXPLORE]: 'Explore' -} - const formatResponseDate = (date: string): string => new Date(date).toLocaleDateString() const getCustomQuestionsText = (response: GuestbookResponse): string => { @@ -80,6 +73,11 @@ export const GuestbookResponses = ({ offset: paginationInfo.offset }) const isLoadingData = isLoading || isLoadingGuestbook || isLoadingGuestbookResponses + const getEventTypeLabel = useCallback( + (eventType: EventType): string => + t(`responses.eventTypes.${eventType}`, { defaultValue: eventType }), + [t] + ) useEffect(() => { setIsLoading(isLoadingData) @@ -118,7 +116,7 @@ export const GuestbookResponses = ({ }) return sortDirection === 'asc' ? sorted : sorted.reverse() - }, [guestbookResponses, sortBy, sortDirection]) + }, [getEventTypeLabel, guestbookResponses, sortBy, sortDirection]) const handleSort = (column: SortableColumn) => { if (sortBy === column) { @@ -332,5 +330,3 @@ export const GuestbookResponses = ({ ) } - -const getEventTypeLabel = (eventType: EventType): string => eventTypeLabels[eventType] ?? eventType From d740540c02aa2670441a7d3880062bf81fc30f60 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 30 Jul 2026 09:27:26 -0400 Subject: [PATCH 15/15] fix: unit test --- .../guestbooks/edit-guestbook/EditGuestbook.spec.tsx | 6 +++--- .../guestbooks/view-responses/GuestbookResponses.spec.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/component/sections/guestbooks/edit-guestbook/EditGuestbook.spec.tsx b/tests/component/sections/guestbooks/edit-guestbook/EditGuestbook.spec.tsx index d5a994b3b..9f87ccf9e 100644 --- a/tests/component/sections/guestbooks/edit-guestbook/EditGuestbook.spec.tsx +++ b/tests/component/sections/guestbooks/edit-guestbook/EditGuestbook.spec.tsx @@ -1,9 +1,9 @@ import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' import { Guestbook } from '@/guestbooks/domain/models/Guestbook' import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' -import { GuestbookRepositoryProvider } from '@/sections/guestbooks/GuestbookRepositoryProvider' import { EditGuestbook } from '@/sections/guestbooks/edit-guestbook/EditGuestbook' import { CollectionMother } from '@tests/component/collection/domain/models/CollectionMother' +import { WithRepositories } from '@tests/component/WithRepositories' const guestbook: Guestbook = { id: 10, @@ -58,13 +58,13 @@ describe('EditGuestbook', () => { const mountComponent = () => cy.customMount( - + - + ) it('prefills fields correctly', () => { diff --git a/tests/component/sections/guestbooks/view-responses/GuestbookResponses.spec.tsx b/tests/component/sections/guestbooks/view-responses/GuestbookResponses.spec.tsx index d53930581..2a41c54cc 100644 --- a/tests/component/sections/guestbooks/view-responses/GuestbookResponses.spec.tsx +++ b/tests/component/sections/guestbooks/view-responses/GuestbookResponses.spec.tsx @@ -3,9 +3,9 @@ import { ReadError } from '@iqss/dataverse-client-javascript' import { Guestbook } from '@/guestbooks/domain/models/Guestbook' import { EventType, GuestbookResponse } from '@/guestbooks/domain/models/GuestbookResponse' import { GuestbookRepository } from '@/guestbooks/domain/repositories/GuestbookRepository' -import { GuestbookRepositoryProvider } from '@/sections/guestbooks/GuestbookRepositoryProvider' import { GuestbookResponses } from '@/sections/guestbooks/view-responses/GuestbookResponses' import { CollectionMother } from '@tests/component/collection/domain/models/CollectionMother' +import { WithRepositories } from '@tests/component/WithRepositories' const guestbook: Guestbook = { id: 10, @@ -97,13 +97,13 @@ describe('GuestbookResponses', () => { const mountComponent = () => cy.customMount( - + - + ) it('renders guestbook response page details and rows', () => {