diff --git a/eslint.config.js b/eslint.config.js index 092408a..ab679c5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,6 +23,12 @@ export default tseslint.config( 'warn', { allowConstantExport: true }, ], + 'no-restricted-imports': [ + 'warn', + { + patterns: [], + }, + ], }, }, ) diff --git a/package-lock.json b/package-lock.json index 4f48a98..60224ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,21 +41,33 @@ }, "devDependencies": { "@eslint/js": "^9.30.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^24.7.2", "@types/proj4": "^2.5.6", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", + "@vitest/coverage-v8": "^2.1.9", "eslint": "^9.30.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "jsdom": "^25.0.1", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", "vite": "^5.4.10", - "vite-plugin-html": "^3.2.2" + "vite-plugin-html": "^3.2.2", + "vitest": "^2.1.9" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -70,6 +82,25 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/@azure/msal-browser": { "version": "4.16.0", "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.16.0.tgz", @@ -338,6 +369,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -386,6 +426,122 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -997,6 +1153,32 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", @@ -1030,11 +1212,10 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "dev": true, - "license": "MIT" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.29", @@ -1106,6 +1287,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@react-leaflet/core": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", @@ -1462,6 +1653,98 @@ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1983,6 +2266,165 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.12", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", @@ -2054,6 +2496,15 @@ "node": ">=0.8" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2071,6 +2522,15 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2094,12 +2554,30 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -2299,6 +2777,15 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2392,6 +2879,22 @@ "node": ">=0.8" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2449,6 +2952,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -2665,6 +3177,31 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -2781,6 +3318,19 @@ "node": ">=12" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -2798,6 +3348,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -2816,6 +3372,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2886,6 +3451,13 @@ "jszip": ">=3.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "peer": true + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -3009,6 +3581,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3047,6 +3625,12 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -3084,6 +3668,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": 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", @@ -3393,6 +3983,15 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==" }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -3683,6 +4282,22 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -3844,6 +4459,27 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3857,6 +4493,30 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "16.3.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", @@ -3995,6 +4655,24 @@ "he": "bin/he" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -4043,6 +4721,32 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4106,6 +4810,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -4179,6 +4892,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "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, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4224,6 +4946,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -4236,6 +4964,71 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "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, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jake": { "version": "10.9.4", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", @@ -4282,6 +5075,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4552,6 +5385,12 @@ "underscore": "^1.13.1" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -4572,6 +5411,63 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "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, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mammoth": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz", @@ -5569,6 +6465,15 @@ "node": ">= 0.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5582,6 +6487,15 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/monaco-editor": { "version": "0.55.1", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", @@ -5716,6 +6630,12 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5804,6 +6724,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -5858,6 +6784,30 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5906,6 +6856,28 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -5919,6 +6891,15 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pg": { "version": "8.16.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", @@ -6106,6 +7087,41 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "peer": true + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6452,6 +7468,19 @@ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", @@ -6607,6 +7636,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6657,6 +7692,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -6848,6 +7895,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6914,6 +7979,12 @@ "node": ">=0.8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, "node_modules/state-local": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", @@ -6928,6 +7999,12 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -6941,6 +8018,56 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "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, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "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 + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -6955,6 +8082,58 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6999,6 +8178,12 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "node_modules/terser": { "version": "5.44.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", @@ -7025,11 +8210,118 @@ "dev": true, "license": "MIT" }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7052,6 +8344,30 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -7469,6 +8785,34 @@ } } }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, "node_modules/vite-plugin-html": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/vite-plugin-html/-/vite-plugin-html-3.2.2.tgz", @@ -7493,6 +8837,145 @@ "vite": ">=2.0.0" } }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "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, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7509,6 +8992,22 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wkt-parser": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.5.2.tgz", @@ -7541,6 +9040,106 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "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 + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "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, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", @@ -7561,6 +9160,15 @@ "node": ">=0.8" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/xmlbuilder": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", @@ -7569,6 +9177,12 @@ "node": ">=4.0" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "node_modules/xstate": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.20.1.tgz", diff --git a/package.json b/package.json index e94b6ae..2145710 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,11 @@ "build:prod": "tsc -b && vite build --mode prod", "build:int": "tsc -b && vite build --mode int", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@azure/msal-browser": "^4.12.0", @@ -47,18 +51,24 @@ }, "devDependencies": { "@eslint/js": "^9.30.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^24.7.2", "@types/proj4": "^2.5.6", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", + "@vitest/coverage-v8": "^2.1.9", "eslint": "^9.30.1", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "jsdom": "^25.0.1", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", "vite": "^5.4.10", - "vite-plugin-html": "^3.2.2" + "vite-plugin-html": "^3.2.2", + "vitest": "^2.1.9" } } diff --git a/src/App.tsx b/src/App.tsx index a677a0d..aac8210 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -186,6 +186,10 @@ function App() { } /> } /> + {/* Redmine Feature Views */} + } /> + } /> + {/* Catch-all für unbekannte Sub-Pfade */} } /> diff --git a/src/api.ts b/src/api.ts index 69ac956..ac02d60 100644 --- a/src/api.ts +++ b/src/api.ts @@ -46,7 +46,14 @@ import { getApiBaseUrl } from '../config/config'; const api = axios.create({ baseURL: getApiBaseUrl(), - withCredentials: true + withCredentials: true, + // FastAPI expects repeat-style array query params (``?ids=1&ids=2``). + // Axios v1.x default would render ``?ids[]=1&ids[]=2``, which FastAPI + // silently drops -- e.g. ``trackerIds`` filters on the Redmine stats + // endpoint never reach the route. Setting ``indexes: null`` switches + // the URLSearchParams visitor to repeat format. Applies globally so + // every endpoint with array query params gets it for free. + paramsSerializer: { indexes: null }, }); // Add a request interceptor to add the auth token, context headers, and log backend IP @@ -92,6 +99,20 @@ api.interceptors.request.use( config.headers['Accept-Language'] = appLanguage; } + // Send browser IANA timezone (e.g. "Europe/Zurich") so the gateway can + // resolve "now" for AI agents and user-visible time strings without + // hardcoding a server-side default. Mirrors the Accept-Language pattern. + if (config.headers) { + try { + const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + if (browserTimezone) { + config.headers['X-User-Timezone'] = browserTimezone; + } + } catch { + // Older browsers without Intl.DateTimeFormat: backend falls back to UTC + } + } + // Add multi-tenant context headers from URL (if not already set) // This ensures Feature-Instance roles are loaded for permission checks const context = getContextFromUrl(); diff --git a/src/api/attributesApi.ts b/src/api/attributesApi.ts index 1776fb9..b01f5b9 100644 --- a/src/api/attributesApi.ts +++ b/src/api/attributesApi.ts @@ -1,4 +1,7 @@ import { ApiRequestOptions } from '../hooks/useApi'; +import type { AttributeType } from '../utils/attributeTypeMapper'; + +export type { AttributeType }; // ============================================================================ // TYPES & INTERFACES @@ -7,7 +10,7 @@ import { ApiRequestOptions } from '../hooks/useApi'; export interface AttributeDefinition { name: string; label: string; - type: 'string' | 'number' | 'date' | 'boolean' | 'enum' | 'text' | 'email' | 'checkbox' | 'select' | 'multiselect' | 'textarea'; + type: AttributeType; sortable?: boolean; filterable?: boolean; searchable?: boolean; diff --git a/src/api/billingApi.ts b/src/api/billingApi.ts index 930749d..cc05e44 100644 --- a/src/api/billingApi.ts +++ b/src/api/billingApi.ts @@ -29,7 +29,7 @@ export interface BillingTransaction { aicoreProvider?: string; aicoreModel?: string; createdByUserId?: string; - createdAt?: string; + sysCreatedAt?: string; mandateId?: string; mandateName?: string; userId?: string; diff --git a/src/api/connectionApi.ts b/src/api/connectionApi.ts index 7263e95..41a79e4 100644 --- a/src/api/connectionApi.ts +++ b/src/api/connectionApi.ts @@ -4,10 +4,26 @@ import { ApiRequestOptions } from '../hooks/useApi'; // TYPES & INTERFACES // ============================================================================ +export interface KnowledgePreferences { + schemaVersion?: number; + neutralizeBeforeEmbed?: boolean; + mailContentDepth?: 'metadata' | 'snippet' | 'full'; + mailIndexAttachments?: boolean; + filesIndexBinaries?: boolean; + mimeAllowlist?: string[]; + clickupScope?: 'titles' | 'title_description' | 'with_comments'; + clickupIndexAttachments?: boolean; + surfaceToggles?: { + google?: { gmail?: boolean; drive?: boolean }; + msft?: { sharepoint?: boolean; outlook?: boolean }; + }; + maxAgeDays?: number; +} + export interface Connection { id: string; userId: string; - authority: 'local' | 'google' | 'msft' | 'clickup'; + authority: 'local' | 'google' | 'msft' | 'clickup' | 'infomaniak'; externalId: string; externalUsername: string; externalEmail?: string; @@ -15,6 +31,8 @@ export interface Connection { connectedAt: number; // Backend uses float for UTC timestamp in seconds lastChecked: number; // Backend uses float for UTC timestamp in seconds expiresAt?: number; // Backend uses Optional[float] for UTC timestamp in seconds + knowledgeIngestionEnabled?: boolean; + knowledgePreferences?: KnowledgePreferences | null; [key: string]: any; // Allow additional properties } @@ -37,6 +55,19 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; + /** Scope request to items of this group (resolved server-side to itemIds IN-filter). */ + groupId?: string; + /** If set, persist this group tree on the backend before fetching (optimistic save). */ + saveGroupTree?: TableGroupNode[]; +} + +export interface TableGroupNode { + id: string; + name: string; + itemIds: string[]; + subGroups: TableGroupNode[]; + order: number; + isExpanded: boolean; } export interface PaginatedResponse { @@ -47,17 +78,21 @@ export interface PaginatedResponse { totalItems: number; totalPages: number; }; + /** Current group tree for this (user, contextKey) pair — undefined if no grouping configured. */ + groupTree?: TableGroupNode[]; } export interface CreateConnectionData { id?: string; userId?: string; - authority?: 'msft' | 'google' | 'clickup'; - type?: 'msft' | 'google' | 'clickup'; // Backend maps type → authority + authority?: 'msft' | 'google' | 'clickup' | 'infomaniak'; + type?: 'msft' | 'google' | 'clickup' | 'infomaniak'; // Backend maps type → authority externalId?: string; externalUsername?: string; externalEmail?: string; status?: 'active' | 'expired' | 'revoked' | 'pending'; + knowledgeIngestionEnabled?: boolean; + knowledgePreferences?: KnowledgePreferences | null; connectedAt?: number; lastChecked?: number; expiresAt?: number; @@ -103,6 +138,8 @@ export async function fetchConnections( if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; + if (params.groupId) paginationObj.groupId = params.groupId; + if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); @@ -136,14 +173,20 @@ export async function createConnection( /** * Connect to a service (initiate OAuth) * Endpoint: POST /api/connections/{connectionId}/connect + * + * @param reauth If true, forces the OAuth provider to re-show the consent screen. + * Required when newly added scopes (e.g. Calendar/Contacts after a + * feature rollout) need to be granted on top of the existing token. */ export async function connectService( request: ApiRequestFunction, - connectionId: string + connectionId: string, + reauth: boolean = false ): Promise { return await request({ url: `/api/connections/${connectionId}/connect`, - method: 'post' + method: 'post', + data: reauth ? { reauth: true } : undefined, }); } @@ -221,3 +264,28 @@ export async function refreshGoogleToken( }); } +/** + * Submit an Infomaniak Personal Access Token (kdrive + mail) for an existing + * UserConnection. The backend validates the token via /1/profile and stores it + * as the connection's data-access bearer token. + * Endpoint: POST /api/infomaniak/connections/{connectionId}/token + */ +export async function submitInfomaniakToken( + request: ApiRequestFunction, + connectionId: string, + token: string +): Promise<{ + id: string; + status: string; + type: string; + externalUsername: string; + externalEmail?: string | null; + lastChecked: number; +}> { + return await request({ + url: `/api/infomaniak/connections/${connectionId}/token`, + method: 'post', + data: { token } + }); +} + diff --git a/src/api/fileApi.ts b/src/api/fileApi.ts index 18dc47e..7e2c67a 100644 --- a/src/api/fileApi.ts +++ b/src/api/fileApi.ts @@ -34,6 +34,8 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; + groupId?: string; + saveGroupTree?: any[]; } export interface PaginatedResponse { @@ -103,6 +105,8 @@ export async function fetchFiles( if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; + if (params.groupId) paginationObj.groupId = params.groupId; + if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); @@ -186,110 +190,87 @@ export async function deleteFiles( return uniqueIds.map(fileId => ({ success: true, fileId })); } -export async function deleteFolders( - request: ApiRequestFunction, - folderIds: string[], - recursiveFolders: boolean = true -): Promise<{ deletedFiles: number; deletedFolders: number }> { - const uniqueIds = [...new Set(folderIds.filter(Boolean))]; - if (uniqueIds.length === 0) return { deletedFiles: 0, deletedFolders: 0 }; - return await request({ - url: '/api/files/batch-delete', - method: 'post', - data: { folderIds: uniqueIds, recursiveFolders } - }); -} - // ============================================================================ -// FOLDER API FUNCTIONS +// GROUP BULK API FUNCTIONS // ============================================================================ -export interface FolderInfo { - id: string; - name: string; - parentId: string | null; - fileCount?: number; - mandateId?: string; - featureInstanceId?: string; - createdAt?: number; - scope?: string; - neutralize?: boolean; -} - -export async function fetchFolders( +/** Patch scope for all files in a group (recursive) */ +export async function patchGroupScope( request: ApiRequestFunction, - parentId?: string | null -): Promise { - const params: any = {}; - if (parentId !== undefined && parentId !== null) { - params.parentId = parentId; - } - const data = await request({ - url: '/api/files/folders', - method: 'get', - params, - }); - return Array.isArray(data) ? data : []; -} - -export async function createFolder( - request: ApiRequestFunction, - name: string, - parentId?: string | null -): Promise { - return await request({ - url: '/api/files/folders', - method: 'post', - data: { name, parentId: parentId || null }, - }); -} - -export async function renameFolder( - request: ApiRequestFunction, - folderId: string, - name: string + groupId: string, + scope: string ): Promise { return await request({ - url: `/api/files/folders/${folderId}`, - method: 'put', - data: { name }, + url: `/api/files/groups/${groupId}/scope`, + method: 'patch', + data: { scope }, }); } -export async function deleteFolderApi( +/** Patch neutralize for all files in a group (recursive, incl. knowledge purge/reindex) */ +export async function patchGroupNeutralize( request: ApiRequestFunction, - folderId: string, - recursive: boolean = false + groupId: string, + neutralize: boolean ): Promise { return await request({ - url: `/api/files/folders/${folderId}`, + url: `/api/files/groups/${groupId}/neutralize`, + method: 'patch', + data: { neutralize }, + }); +} + +/** Download all files in a group as ZIP */ +export async function downloadGroupZip(groupId: string): Promise { + const { default: api } = await import('../api'); + const response = await api.get(`/api/files/groups/${groupId}/download`, { + responseType: 'blob', + }); + const url = window.URL.createObjectURL(response.data); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `group-${groupId}.zip`); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); +} + +/** Delete a group and optionally all its files */ +export async function deleteGroup( + request: ApiRequestFunction, + groupId: string, + deleteItems: boolean = false +): Promise { + return await request({ + url: `/api/files/groups/${groupId}`, method: 'delete', - params: { recursive }, + params: { deleteItems }, }); } -export async function moveFolder( - request: ApiRequestFunction, - folderId: string, - targetParentId: string | null -): Promise { - return await request({ - url: `/api/files/folders/${folderId}/move`, - method: 'post', - data: { targetParentId }, - }); -} - -export async function moveFile( - request: ApiRequestFunction, - fileId: string, - targetFolderId: string | null -): Promise { - return await request({ - url: `/api/files/${fileId}/move`, - method: 'post', - data: { targetFolderId }, - }); +/** Collect all file IDs belonging to a group recursively (client-side, from known groupTree) */ +export function collectGroupItemIds( + groupTree: Array<{ id: string; itemIds: string[]; subGroups: any[] }>, + groupId: string +): string[] { + const collect = (nodes: Array<{ id: string; itemIds: string[]; subGroups: any[] }>): string[] | null => { + for (const node of nodes) { + if (node.id === groupId) { + const ids: string[] = [...node.itemIds]; + const sub = (n: { id: string; itemIds: string[]; subGroups: any[] }) => { + ids.push(...n.itemIds); + n.subGroups.forEach(sub); + }; + node.subGroups.forEach(sub); + return ids; + } + const found = collect(node.subGroups); + if (found) return found; + } + return null; + }; + return collect(groupTree) ?? []; } // Note: The following operations require special handling (FormData, blob responses) @@ -299,3 +280,121 @@ export async function moveFile( // - previewFile: Requires flexible responseType (json or blob) // These are kept in the hooks for now due to their special requirements +// ============================================================================ +// FOLDER TYPES & API FUNCTIONS +// ============================================================================ + +export interface FolderInfo { + id: string; + name: string; + parentId: string | null; + mandateId: string; + featureInstanceId: string; + scope: string; + neutralize: boolean; + contextOrphan?: boolean; + sysCreatedBy?: string; + sysCreatedAt?: number; + sysModifiedAt?: number; +} + +export async function getFolderTree( + request: ApiRequestFunction, + owner: 'me' | 'shared' = 'me', +): Promise { + const data = await request({ + url: '/api/files/folders/tree', + method: 'get', + params: { owner }, + }); + return Array.isArray(data) ? data : []; +} + +export async function createFolder( + request: ApiRequestFunction, + name: string, + parentId?: string | null, +): Promise { + return await request({ + url: '/api/files/folders', + method: 'post', + data: { name, parentId: parentId ?? null }, + }); +} + +export async function renameFolder( + request: ApiRequestFunction, + folderId: string, + name: string, +): Promise { + return await request({ + url: `/api/files/folders/${folderId}`, + method: 'patch', + data: { name }, + }); +} + +export async function moveFolder( + request: ApiRequestFunction, + folderId: string, + parentId: string | null, +): Promise { + return await request({ + url: `/api/files/folders/${folderId}/move`, + method: 'post', + data: { parentId }, + }); +} + +export async function deleteFolderCascade( + request: ApiRequestFunction, + folderId: string, +): Promise<{ deletedFolders: number; deletedFiles: number }> { + return await request({ + url: `/api/files/folders/${folderId}`, + method: 'delete', + params: { cascade: true }, + }); +} + +export async function patchFolderScope( + request: ApiRequestFunction, + folderId: string, + scope: string, + cascadeToFiles: boolean = false, +): Promise<{ folderId: string; scope: string; filesUpdated: number }> { + return await request({ + url: `/api/files/folders/${folderId}/scope`, + method: 'patch', + data: { scope, cascadeToFiles }, + }); +} + +export async function patchFolderNeutralize( + request: ApiRequestFunction, + folderId: string, + neutralize: boolean, +): Promise<{ folderId: string; neutralize: boolean; filesUpdated: number }> { + return await request({ + url: `/api/files/folders/${folderId}/neutralize`, + method: 'patch', + data: { neutralize }, + }); +} + +export async function moveFiles( + request: ApiRequestFunction, + fileIds: string[], + targetFolderId: string | null, +): Promise { + await Promise.all( + fileIds.map((fileId) => + request({ + url: `/api/files/${fileId}`, + method: 'put', + data: { folderId: targetFolderId }, + }), + ), + ); +} + diff --git a/src/api/mandateApi.ts b/src/api/mandateApi.ts index 38bf41c..7946395 100644 --- a/src/api/mandateApi.ts +++ b/src/api/mandateApi.ts @@ -46,6 +46,8 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; + groupId?: string; + saveGroupTree?: any[]; } export interface PaginatedResponse { @@ -84,6 +86,8 @@ export async function fetchMandates( if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; + if (params.groupId) paginationObj.groupId = params.groupId; + if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); diff --git a/src/api/promptApi.ts b/src/api/promptApi.ts index 00f1be7..e735ae0 100644 --- a/src/api/promptApi.ts +++ b/src/api/promptApi.ts @@ -49,6 +49,8 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; + groupId?: string; + saveGroupTree?: any[]; } export interface PaginatedResponse { @@ -110,6 +112,8 @@ export async function fetchPrompts( if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; + if (params.groupId) paginationObj.groupId = params.groupId; + if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); diff --git a/src/api/redmineApi.ts b/src/api/redmineApi.ts new file mode 100644 index 0000000..39bc545 --- /dev/null +++ b/src/api/redmineApi.ts @@ -0,0 +1,398 @@ +/** + * Redmine API + * + * Frontend client for the Redmine feature backend. + * URL pattern: /api/redmine/{instanceId}/... + */ + +import { ApiRequestOptions } from '../hooks/useApi'; + +// ============================================================================ +// Types -- mirror gateway/modules/features/redmine/datamodelRedmine.py +// ============================================================================ + +export interface RedmineConfigDto { + id?: string; + featureInstanceId: string; + mandateId?: string | null; + baseUrl: string; + projectId: string; + hasApiKey: boolean; + rootTrackerName: string; + defaultPeriodValue?: Record | null; + schemaCacheTtlSeconds: number; + schemaCachedAt?: number | null; + isActive: boolean; + lastConnectedAt?: number | null; + lastSyncAt?: number | null; + lastFullSyncAt?: number | null; + lastSyncTicketCount?: number | null; + lastSyncErrorMessage?: string | null; +} + +export interface RedmineConfigUpdateRequest { + baseUrl?: string; + projectId?: string; + apiKey?: string; + rootTrackerName?: string; + defaultPeriodValue?: Record | null; + schemaCacheTtlSeconds?: number; + isActive?: boolean; +} + +export interface RedmineFieldChoice { + id: number; + name: string; + isClosed?: boolean | null; +} + +export interface RedmineCustomFieldSchema { + id: number; + name: string; + fieldFormat: string; + isRequired: boolean; + possibleValues: string[]; + multiple: boolean; + defaultValue?: string | null; +} + +export interface RedmineFieldSchema { + projectId: string; + projectName: string; + trackers: RedmineFieldChoice[]; + statuses: RedmineFieldChoice[]; + priorities: RedmineFieldChoice[]; + users: RedmineFieldChoice[]; + categories: RedmineFieldChoice[]; + customFields: RedmineCustomFieldSchema[]; + rootTrackerName: string; + rootTrackerId: number | null; +} + +export interface RedmineRelation { + id: number; + issueId: number; + issueToId: number; + relationType: string; + delay?: number | null; +} + +export interface RedmineCustomFieldValue { + id: number; + name: string; + value: any; +} + +export interface RedmineTicket { + id: number; + subject: string; + description: string; + trackerId?: number | null; + trackerName?: string | null; + statusId?: number | null; + statusName?: string | null; + isClosed: boolean; + priorityId?: number | null; + priorityName?: string | null; + assignedToId?: number | null; + assignedToName?: string | null; + authorId?: number | null; + authorName?: string | null; + parentId?: number | null; + fixedVersionId?: number | null; + fixedVersionName?: string | null; + categoryId?: number | null; + categoryName?: string | null; + createdOn?: string | null; + updatedOn?: string | null; + customFields: RedmineCustomFieldValue[]; + relations: RedmineRelation[]; +} + +export interface RedmineSyncResult { + instanceId: string; + full: boolean; + ticketsUpserted: number; + relationsUpserted: number; + durationMs: number; + lastSyncAt: number; + error?: string | null; +} + +export interface RedmineSyncStatus { + instanceId: string; + lastSyncAt?: number | null; + lastFullSyncAt?: number | null; + lastSyncDurationMs?: number | null; + lastSyncTicketCount?: number | null; + lastSyncErrorAt?: number | null; + lastSyncErrorMessage?: string | null; + mirroredTicketCount: number; + mirroredRelationCount: number; +} + +export interface RedmineConnectionTestResult { + ok: boolean; + reason?: string; + message?: string; + status?: number; + user?: { id: number; name: string }; + project?: { id: number; name: string }; +} + +export interface RedmineStats { + instanceId: string; + dateFrom?: string | null; + dateTo?: string | null; + bucket: string; + trackerIds: number[]; + categoryIds: number[]; + statusFilter: string; + kpis: { + total: number; + open: number; + closed: number; + closedInPeriod: number; + createdInPeriod: number; + orphans: number; + }; + statusByTracker: Array<{ + trackerId?: number | null; + trackerName: string; + countsByStatus: Record; + total: number; + }>; + throughput: Array<{ + bucketKey: string; + label: string; + created: number; + closed: number; + cumTotal: number; + cumOpen: number; + }>; + topAssignees: Array<{ + assignedToId?: number | null; + name: string; + open: number; + }>; + relationDistribution: Array<{ relationType: string; count: number }>; + backlogAging: Array<{ + bucketKey: string; + label: string; + minDays: number; + maxDays?: number | null; + count: number; + }>; +} + +export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; + +const _baseUrl = (instanceId: string): string => `/api/redmine/${instanceId}`; + +// ============================================================================ +// Config +// ============================================================================ + +export async function getRedmineConfigApi( + request: ApiRequestFunction, + instanceId: string, +): Promise { + return await request({ url: `${_baseUrl(instanceId)}/config`, method: 'get' }); +} + +export async function updateRedmineConfigApi( + request: ApiRequestFunction, + instanceId: string, + body: RedmineConfigUpdateRequest, +): Promise { + return await request({ url: `${_baseUrl(instanceId)}/config`, method: 'put', data: body }); +} + +export async function deleteRedmineConfigApi( + request: ApiRequestFunction, + instanceId: string, +): Promise<{ deleted: boolean }> { + return await request({ url: `${_baseUrl(instanceId)}/config`, method: 'delete' }); +} + +export async function testRedmineConnectionApi( + request: ApiRequestFunction, + instanceId: string, +): Promise { + return await request({ url: `${_baseUrl(instanceId)}/config/test`, method: 'post' }); +} + +// ============================================================================ +// Schema +// ============================================================================ + +export async function getRedmineSchemaApi( + request: ApiRequestFunction, + instanceId: string, + forceRefresh = false, +): Promise { + return await request({ + url: `${_baseUrl(instanceId)}/schema`, + method: 'get', + params: forceRefresh ? { forceRefresh: true } : undefined, + }); +} + +// ============================================================================ +// Sync +// ============================================================================ + +export async function runRedmineSyncApi( + request: ApiRequestFunction, + instanceId: string, + force = false, +): Promise { + return await request({ + url: `${_baseUrl(instanceId)}/sync`, + method: 'post', + params: force ? { force: true } : undefined, + }); +} + +export async function getRedmineSyncStatusApi( + request: ApiRequestFunction, + instanceId: string, +): Promise { + return await request({ url: `${_baseUrl(instanceId)}/sync/status`, method: 'get' }); +} + +// ============================================================================ +// Tickets +// ============================================================================ + +export interface ListTicketsParams { + trackerIds?: number[]; + status?: 'open' | 'closed' | '*'; + dateFrom?: string; + dateTo?: string; + assignedToId?: number; +} + +export async function listRedmineTicketsApi( + request: ApiRequestFunction, + instanceId: string, + params: ListTicketsParams = {}, +): Promise { + const queryParams: Record = {}; + if (params.status) queryParams.status = params.status; + if (params.dateFrom) queryParams.dateFrom = params.dateFrom; + if (params.dateTo) queryParams.dateTo = params.dateTo; + if (params.assignedToId !== undefined) queryParams.assignedToId = params.assignedToId; + if (params.trackerIds && params.trackerIds.length > 0) queryParams.trackerIds = params.trackerIds; + return await request({ + url: `${_baseUrl(instanceId)}/tickets`, + method: 'get', + params: queryParams, + }); +} + +export async function getRedmineTicketApi( + request: ApiRequestFunction, + instanceId: string, + issueId: number, +): Promise { + return await request({ + url: `${_baseUrl(instanceId)}/tickets/${issueId}`, + method: 'get', + }); +} + +export interface RedmineTicketUpdateBody { + subject?: string; + description?: string; + trackerId?: number; + statusId?: number; + priorityId?: number; + assignedToId?: number; + parentIssueId?: number; + fixedVersionId?: number; + notes?: string; + customFields?: Record; +} + +export async function updateRedmineTicketApi( + request: ApiRequestFunction, + instanceId: string, + issueId: number, + body: RedmineTicketUpdateBody, +): Promise { + return await request({ + url: `${_baseUrl(instanceId)}/tickets/${issueId}`, + method: 'put', + data: body, + }); +} + +export interface RedmineTicketCreateBody { + subject: string; + trackerId: number; + description?: string; + statusId?: number; + priorityId?: number; + assignedToId?: number; + parentIssueId?: number; + fixedVersionId?: number; + customFields?: Record; +} + +export async function createRedmineTicketApi( + request: ApiRequestFunction, + instanceId: string, + body: RedmineTicketCreateBody, +): Promise { + return await request({ + url: `${_baseUrl(instanceId)}/tickets`, + method: 'post', + data: body, + }); +} + +export async function deleteRedmineTicketApi( + request: ApiRequestFunction, + instanceId: string, + issueId: number, + fallbackStatusId?: number, +): Promise<{ deleted: boolean; archived: boolean; statusId: number | null }> { + return await request({ + url: `${_baseUrl(instanceId)}/tickets/${issueId}`, + method: 'delete', + params: fallbackStatusId !== undefined ? { fallbackStatusId } : undefined, + }); +} + +// ============================================================================ +// Stats +// ============================================================================ + +export interface RedmineStatsParams { + dateFrom?: string; + dateTo?: string; + bucket?: 'day' | 'week' | 'month'; + trackerIds?: number[]; + categoryIds?: number[]; + statusFilter?: '*' | 'open' | 'closed'; +} + +export async function getRedmineStatsApi( + request: ApiRequestFunction, + instanceId: string, + params: RedmineStatsParams = {}, +): Promise { + const queryParams: Record = {}; + if (params.dateFrom) queryParams.dateFrom = params.dateFrom; + if (params.dateTo) queryParams.dateTo = params.dateTo; + if (params.bucket) queryParams.bucket = params.bucket; + if (params.trackerIds && params.trackerIds.length > 0) queryParams.trackerIds = params.trackerIds; + if (params.categoryIds && params.categoryIds.length > 0) queryParams.categoryIds = params.categoryIds; + if (params.statusFilter && params.statusFilter !== '*') queryParams.statusFilter = params.statusFilter; + return await request({ + url: `${_baseUrl(instanceId)}/stats`, + method: 'get', + params: queryParams, + }); +} diff --git a/src/api/teamsbotApi.ts b/src/api/teamsbotApi.ts index b6387ac..462268d 100644 --- a/src/api/teamsbotApi.ts +++ b/src/api/teamsbotApi.ts @@ -169,11 +169,63 @@ export interface MfaChallengeEvent { // SSE Event Types export interface TeamsbotSSEEvent { - type: 'transcript' | 'botResponse' | 'analysis' | 'suggestedResponse' | 'statusChange' | 'error' | 'ping' | 'sessionState' | 'ttsDeliveryStatus' | 'mfaChallenge' | 'mfaResolved' | 'chatSendFailed'; + type: + | 'transcript' + | 'botResponse' + | 'analysis' + | 'suggestedResponse' + | 'statusChange' + | 'error' + | 'ping' + | 'sessionState' + | 'ttsDeliveryStatus' + | 'mfaChallenge' + | 'mfaResolved' + | 'chatSendFailed' + | 'directorPrompt' + | 'agentRun' + | 'botConnectionState'; data: any; timestamp?: string; } +// ========================================================================= +// Director Prompts (private operator instructions during a live meeting) +// ========================================================================= + +export type DirectorPromptMode = 'oneShot' | 'persistent'; +export type DirectorPromptStatus = + | 'queued' + | 'running' + | 'succeeded' + | 'failed' + | 'consumed'; + +export const DIRECTOR_PROMPT_TEXT_LIMIT = 8000; +export const DIRECTOR_PROMPT_FILE_LIMIT = 10; + +export interface DirectorPrompt { + id: string; + sessionId: string; + instanceId: string; + operatorUserId: string; + text: string; + mode: DirectorPromptMode; + fileIds: string[]; + status: DirectorPromptStatus; + statusMessage?: string; + createdAt: string; + consumedAt?: string; + agentRunId?: string; + responseText?: string; +} + +export interface DirectorPromptCreateRequest { + text: string; + mode: DirectorPromptMode; + fileIds?: string[]; +} + // ============================================================================ // API FUNCTIONS // ============================================================================ @@ -289,6 +341,29 @@ export async function listSystemBots(instanceId: string): Promise<{ bots: System return response.data; } +/** + * Create a new system bot account. The password is encrypted server-side + * before storage; the API never returns the password back. SysAdmin only. + */ +export async function createSystemBot( + instanceId: string, + payload: { email: string; password: string; name?: string }, +): Promise<{ bot: SystemBot }> { + const response = await api.post(`/api/teamsbot/${instanceId}/system-bots`, payload); + return response.data; +} + +/** + * Delete a system bot account. SysAdmin only. + */ +export async function deleteSystemBot( + instanceId: string, + botId: string, +): Promise<{ deleted: boolean }> { + const response = await api.delete(`/api/teamsbot/${instanceId}/system-bots/${botId}`); + return response.data; +} + /** * Test TTS voice with AI-generated sample text. Returns base64-encoded audio. */ @@ -452,3 +527,50 @@ export async function submitMfaCode( }); return response.data; } + +// ========================================================================= +// Director Prompts +// ========================================================================= + +/** + * Submit a private director prompt to the running bot. Triggers the full + * agent path (web, mail, RAG, etc.) and delivers the answer into the meeting. + */ +export async function submitDirectorPrompt( + instanceId: string, + sessionId: string, + body: DirectorPromptCreateRequest, +): Promise<{ prompt: DirectorPrompt }> { + const response = await api.post( + `/api/teamsbot/${instanceId}/sessions/${sessionId}/directorPrompts`, + body, + ); + return response.data; +} + +/** + * List director prompts for a session (operator's own prompts only). + */ +export async function listDirectorPrompts( + instanceId: string, + sessionId: string, +): Promise<{ prompts: DirectorPrompt[] }> { + const response = await api.get( + `/api/teamsbot/${instanceId}/sessions/${sessionId}/directorPrompts`, + ); + return response.data; +} + +/** + * Remove a (typically persistent) director prompt. + */ +export async function deleteDirectorPrompt( + instanceId: string, + sessionId: string, + promptId: string, +): Promise<{ deleted: boolean; promptId: string }> { + const response = await api.delete( + `/api/teamsbot/${instanceId}/sessions/${sessionId}/directorPrompts/${promptId}`, + ); + return response.data; +} diff --git a/src/api/trusteeApi.ts b/src/api/trusteeApi.ts index c3eb5da..f21a369 100644 --- a/src/api/trusteeApi.ts +++ b/src/api/trusteeApi.ts @@ -853,16 +853,46 @@ export async function fetchChartOfAccounts( }); } +/** + * Submits a background job that pushes positions to the accounting system and + * polls `/api/jobs/{jobId}` until the job reaches a terminal status. Returns + * the same `{ total, success, skipped, errors, results }` payload that the + * legacy synchronous endpoint used to return -- but does NOT block the user + * while the (potentially long) external accounting calls run in the worker. + */ export async function syncPositionsToAccounting( request: ApiRequestFunction, instanceId: string, - positionIds: string[] -): Promise<{ total: number; success: number; errors: number; results: any[] }> { - return await request({ + positionIds: string[], + opts?: { pollMs?: number; onProgress?: (progress: number, message?: string | null) => void } +): Promise<{ total: number; success: number; skipped?: number; errors: number; results: any[] }> { + const submission = await request({ url: `${_getTrusteeBaseUrl(instanceId)}/accounting/sync`, method: 'post', data: { positionIds } }); + + const jobId: string | undefined = submission?.jobId; + if (!jobId) { + throw new Error('Background job could not be started (missing jobId).'); + } + + const pollMs = opts?.pollMs ?? 1500; + const TERMINAL = new Set(['SUCCESS', 'ERROR', 'CANCELLED']); + + while (true) { + const job = await request({ url: `/api/jobs/${jobId}`, method: 'get' }); + if (opts?.onProgress) { + opts.onProgress(Number(job?.progress ?? 0), job?.progressMessage ?? null); + } + if (job?.status && TERMINAL.has(job.status)) { + if (job.status === 'SUCCESS' && job.result) { + return job.result; + } + throw new Error(job?.errorMessage || 'Sync-Job fehlgeschlagen'); + } + await new Promise((resolve) => setTimeout(resolve, pollMs)); + } } export async function fetchSyncStatus( diff --git a/src/api/userApi.ts b/src/api/userApi.ts index d16bf38..98dd7a2 100644 --- a/src/api/userApi.ts +++ b/src/api/userApi.ts @@ -48,6 +48,8 @@ export interface PaginationParams { sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; + groupId?: string; + saveGroupTree?: any[]; } export interface PaginatedResponse { @@ -152,6 +154,8 @@ export async function fetchUsers( if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; + if (params.groupId) paginationObj.groupId = params.groupId; + if (params.saveGroupTree !== undefined) paginationObj.saveGroupTree = params.saveGroupTree; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); diff --git a/src/api/workflowApi.ts b/src/api/workflowApi.ts index 8c7a9e2..7a03b4e 100644 --- a/src/api/workflowApi.ts +++ b/src/api/workflowApi.ts @@ -26,8 +26,12 @@ export interface NodeTypeParameter { export interface PortField { name: string; type: string; - description: Record; + /** Plain string or per-language map from the API catalog. */ + description: string | Record; required: boolean; + enumValues?: string[] | null; + /** When true, surface at the top of the DataPicker as the most common/recommended pick. */ + recommended?: boolean; } export interface PortSchema { @@ -39,8 +43,14 @@ export interface InputPortDef { accepts: string[]; } +/** Graph-defined output schema (e.g. form fields from node parameters). */ +export interface GraphDefinedSchemaRef { + kind: 'fromGraph'; + parameter: string; +} + export interface OutputPortDef { - schema: string; + schema: string | GraphDefinedSchemaRef; dynamic?: boolean; deriveFrom?: string; } @@ -77,11 +87,19 @@ export interface SystemVariable { description: string; } +/** Single form field type with its canonical port primitive. Delivered by GET /node-types. */ +export interface FormFieldType { + id: string; + label: string; + portType: string; +} + export interface NodeTypesResponse { nodeTypes: NodeType[]; categories: NodeTypeCategory[]; portTypeCatalog?: Record; systemVariables?: Record; + formFieldTypes?: FormFieldType[]; } export interface Automation2GraphNode { @@ -89,7 +107,7 @@ export interface Automation2GraphNode { type: string; parameters?: Record; inputPorts?: Array<{ name: string; schema: string; accepts?: string[] }>; - outputPorts?: Array<{ name: string; schema: string }>; + outputPorts?: Array<{ name: string; schema: string | GraphDefinedSchemaRef }>; } export interface Automation2Connection { @@ -108,6 +126,10 @@ export interface ExecuteGraphResponse { success: boolean; nodeOutputs?: Record; error?: string; + /** Soft, non-blocking message displayed alongside a successful response. + * Used e.g. by the Save flow to surface "Gespeichert mit X Pflicht-Fehlern" + * without flipping `success` to `false`. */ + warning?: string; stopped?: boolean; failedNode?: string; paused?: boolean; @@ -132,6 +154,8 @@ export interface Automation2Workflow { label: string; graph: Automation2Graph; active?: boolean; + /** Target feature instance for execution data scope (NULL for templates) */ + targetFeatureInstanceId?: string | null; /** Entry points (Starts) — how this workflow may be invoked */ invocations?: WorkflowEntryPoint[]; /** Enriched: run count */ @@ -144,8 +168,8 @@ export interface Automation2Workflow { stuckAtNodeId?: string; /** Enriched: human-readable label for stuck node */ stuckAtNodeLabel?: string; - /** Enriched: created timestamp (seconds) */ - createdAt?: number; + /** From PowerOnModel base — record creation timestamp (seconds) */ + sysCreatedAt?: number; /** Enriched: last run started timestamp (seconds) */ lastStartedAt?: number; } @@ -263,8 +287,57 @@ export async function fetchNodeTypes( }); const nodeTypes = data?.nodeTypes ?? []; const categories = data?.categories ?? []; - console.log(`${LOG} fetchNodeTypes response: ${nodeTypes.length} nodeTypes, ${categories.length} categories`); - return { nodeTypes, categories }; + const portTypeCatalog = data?.portTypeCatalog ?? undefined; + const systemVariables = data?.systemVariables ?? undefined; + const formFieldTypes = data?.formFieldTypes ?? undefined; + console.log( + `${LOG} fetchNodeTypes response: ${nodeTypes.length} nodeTypes, ${categories.length} categories, ` + + `${portTypeCatalog ? Object.keys(portTypeCatalog).length : 0} portTypes, ` + + `${systemVariables ? Object.keys(systemVariables).length : 0} sysVars, ` + + `${formFieldTypes ? formFieldTypes.length : 0} formFieldTypes` + ); + return { nodeTypes, categories, portTypeCatalog, systemVariables, formFieldTypes }; +} + +export interface UpstreamPathEntry { + producerNodeId: string; + producerLabel?: string; + path: (string | number)[]; + type: string; + label: string; + scopeOrigin: 'data' | 'loop' | 'system'; +} + +/** + * POST /api/workflows/{instanceId}/upstream-paths — pickable upstream paths for DataPicker / AI. + */ +export async function postUpstreamPaths( + request: ApiRequestFunction, + instanceId: string, + graph: Automation2Graph, + nodeId: string +): Promise<{ paths: UpstreamPathEntry[] }> { + const data = await request({ + url: `/api/workflows/${instanceId}/upstream-paths`, + method: 'post', + data: { graph, nodeId }, + }); + return { paths: (data?.paths ?? []) as UpstreamPathEntry[] }; +} + +/** GET saved workflow graph variant of upstream-paths (requires workflowId). */ +export async function getUpstreamPathsSaved( + request: ApiRequestFunction, + instanceId: string, + workflowId: string, + nodeId: string +): Promise<{ paths: UpstreamPathEntry[] }> { + const data = await request({ + url: `/api/workflows/${instanceId}/upstream-paths/${encodeURIComponent(nodeId)}`, + method: 'get', + params: { workflowId }, + }); + return { paths: (data?.paths ?? []) as UpstreamPathEntry[] }; } /** @@ -353,7 +426,12 @@ export async function fetchWorkflow( export async function createWorkflow( request: ApiRequestFunction, instanceId: string, - body: { label: string; graph: Automation2Graph; invocations?: WorkflowEntryPoint[] } + body: { + label: string; + graph: Automation2Graph; + invocations?: WorkflowEntryPoint[]; + targetFeatureInstanceId?: string | null; + } ): Promise { return await request({ url: `/api/workflows/${instanceId}/workflows`, @@ -372,6 +450,7 @@ export async function updateWorkflow( invocations?: WorkflowEntryPoint[]; active?: boolean; notifyOnFailure?: boolean; + targetFeatureInstanceId?: string | null; } ): Promise { return await request({ @@ -927,3 +1006,95 @@ export async function loadClickupListTasksForDropdown( acc.sort((a, b) => a.name.localeCompare(b.name, 'de')); return acc; } + + +// ============================================================================ +// AUTOMATION WORKSPACE API (user-facing run workspace) +// ============================================================================ + +export interface WorkspaceRun { + id: string; + workflowId: string; + workflowLabel?: string; + status: string; + startedAt?: number; + completedAt?: number; + ownerId?: string; + mandateId?: string; + mandateLabel?: string; + targetFeatureInstanceId?: string; + targetInstanceLabel?: string; + costTokens?: number; + costCredits?: number; + error?: string; +} + +export interface WorkspaceRunDetail { + run: WorkspaceRun & { nodeOutputs?: Record }; + workflow: { + id: string; + label: string; + targetFeatureInstanceId?: string; + featureInstanceId?: string; + tags?: string[]; + } | null; + steps: Array<{ + id: string; + runId: string; + nodeId: string; + nodeType: string; + status: string; + inputSnapshot?: Record; + output?: Record; + inputFiles?: Array<{ id: string; fileName?: string }>; + outputFiles?: Array<{ id: string; fileName?: string }>; + error?: string; + startedAt?: number; + completedAt?: number; + durationMs?: number; + tokensUsed?: number; + retryCount?: number; + }>; + files: Array<{ + id: string; + fileName?: string; + contentType?: string; + sizeBytes?: number; + }>; + unassignedFiles?: Array<{ + id: string; + fileName?: string; + }>; +} + +export async function fetchWorkspaceRuns( + request: ApiRequestFunction, + params: { + scope?: 'mine' | 'mandate'; + status?: string; + targetInstanceId?: string; + workflowId?: string; + limit?: number; + offset?: number; + } = {}, +): Promise<{ runs: WorkspaceRun[]; total: number }> { + const query = new URLSearchParams(); + if (params.scope) query.set('scope', params.scope); + if (params.status) query.set('status', params.status); + if (params.targetInstanceId) query.set('targetInstanceId', params.targetInstanceId); + if (params.workflowId) query.set('workflowId', params.workflowId); + if (params.limit) query.set('limit', String(params.limit)); + if (params.offset) query.set('offset', String(params.offset)); + const qs = query.toString(); + const url = `/api/automations/runs${qs ? `?${qs}` : ''}`; + const resp = await request({ url, method: 'get' }); + return resp as { runs: WorkspaceRun[]; total: number }; +} + +export async function fetchWorkspaceRunDetail( + request: ApiRequestFunction, + runId: string, +): Promise { + const resp = await request({ url: `/api/automations/runs/${runId}/detail`, method: 'get' }); + return resp as WorkspaceRunDetail; +} diff --git a/src/components/AddConnectionWizard/AddConnectionWizard.module.css b/src/components/AddConnectionWizard/AddConnectionWizard.module.css new file mode 100644 index 0000000..5cabd64 --- /dev/null +++ b/src/components/AddConnectionWizard/AddConnectionWizard.module.css @@ -0,0 +1,467 @@ +/* AddConnectionWizard styles */ + +.stepper { + display: flex; + justify-content: center; + gap: 1.5rem; + padding: 1rem 1.5rem 0; + border-bottom: 1px solid var(--border-color); +} + +.stepDot { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 600; + background: var(--bg-secondary, #f0f0f0); + color: var(--text-secondary, #666); + border: 2px solid var(--border-color, #ddd); + transition: background 0.2s, border-color 0.2s, color 0.2s; +} + +.stepDotActive { + background: var(--primary-color, #f25843); + border-color: var(--primary-color, #f25843); + color: white; +} + +.stepDotDone { + background: var(--success-color, #22c55e); + border-color: var(--success-color, #22c55e); + color: white; +} + +.stepDotHidden { + opacity: 0.3; +} + +.body { + padding: 1.5rem; + overflow-y: auto; +} + +.stepContent { + display: flex; + flex-direction: column; + gap: 1rem; + min-height: 220px; +} + +.stepTitle { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.stepBody { + font-size: 0.9375rem; + color: var(--text-primary); + line-height: 1.6; + margin: 0; +} + +.stepHint { + font-size: 0.8125rem; + color: var(--text-secondary, #666); + margin: 0; +} + +/* Connector grid (Step 0) */ +.connectorGrid { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.connectorCard { + flex: 1 1 140px; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.625rem; + padding: 1.25rem 1rem; + background: var(--surface-color); + border: 2px solid var(--border-color, #ddd); + border-radius: 10px; + cursor: pointer; + transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s; +} + +.connectorCard:hover { + border-color: var(--primary-color, #f25843); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + transform: translateY(-2px); +} + +.connectorIcon { + font-size: 1.75rem; +} + +.connectorLabel { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-primary); +} + +/* Consent step (Step 1) */ +.consentIcon { + display: flex; + justify-content: center; + color: var(--primary-color, #f25843); +} + +.consentButtons { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.consentButtonYes { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + background: var(--primary-color, #f25843); + color: white; + border: none; + border-radius: 8px; + font-size: 0.9375rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.consentButtonYes:hover { + background: var(--primary-dark, #d94d3a); +} + +.consentButtonNo { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + background: var(--surface-color); + color: var(--text-primary); + border: 2px solid var(--border-color, #ddd); + border-radius: 8px; + font-size: 0.9375rem; + font-weight: 500; + cursor: pointer; + transition: border-color 0.2s, background 0.2s; +} + +.consentButtonNo:hover { + border-color: var(--text-secondary, #888); + background: var(--bg-secondary, #f5f5f5); +} + +/* Preferences step (Step 2) */ +.prefGroup { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.75rem 0; + border-bottom: 1px solid var(--border-color, #eee); +} + +.prefGroup:last-of-type { + border-bottom: none; +} + +.prefLabel { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + font-size: 0.9375rem; + color: var(--text-primary); + cursor: pointer; + font-weight: 500; +} + +.prefLabelRow { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + font-size: 0.9375rem; + color: var(--text-primary); + font-weight: 500; +} + +.prefIcon { + color: var(--text-secondary, #666); + font-size: 0.875rem; +} + +.prefCheck { + width: 18px; + height: 18px; + cursor: pointer; + accent-color: var(--primary-color, #f25843); +} + +.prefSelect { + padding: 0.375rem 0.5rem; + border: 1px solid var(--border-color, #ddd); + border-radius: 6px; + font-size: 0.875rem; + background: var(--surface-color); + color: var(--text-primary); + min-width: 200px; +} + +.prefNumber { + width: 80px; + padding: 0.375rem 0.5rem; + border: 1px solid var(--border-color, #ddd); + border-radius: 6px; + font-size: 0.875rem; + background: var(--surface-color); + color: var(--text-primary); + text-align: right; +} + +.prefHint { + font-size: 0.8125rem; + color: var(--text-secondary, #666); + margin: 0; +} + +/* Summary step (Step 3) */ +.summary { + display: flex; + flex-direction: column; + gap: 0; + border: 1px solid var(--border-color, #ddd); + border-radius: 8px; + overflow: hidden; +} + +.summaryRow { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.625rem 1rem; + gap: 1rem; + border-bottom: 1px solid var(--border-color, #eee); +} + +.summaryRow:last-child { + border-bottom: none; +} + +.summaryKey { + font-size: 0.875rem; + color: var(--text-secondary, #666); + font-weight: 500; +} + +.summaryVal { + display: flex; + align-items: center; + gap: 0.375rem; + font-size: 0.875rem; + color: var(--text-primary); + font-weight: 500; +} + +/* Back button (step 1 consent screen) */ +.stepNavLeft { + margin-top: 0.75rem; + display: flex; +} + +.navBack { + background: none; + border: none; + padding: 0.25rem 0; + font-size: 0.8125rem; + color: var(--text-secondary, #666); + cursor: pointer; + text-decoration: underline; +} + +.navBack:hover { + color: var(--text-primary); +} + +/* Cost estimate hint */ +.costHint { + display: flex; + align-items: flex-start; + gap: 0.625rem; + padding: 0.75rem 1rem; + background: var(--info-bg, #eff6ff); + border: 1px solid var(--info-border, #bfdbfe); + border-radius: 8px; + font-size: 0.8125rem; +} + +.costHintIcon { + flex-shrink: 0; + margin-top: 2px; + color: var(--info-color, #3b82f6); +} + +.costHint > div { + display: flex; + flex-direction: column; + gap: 0.25rem; + width: 100%; +} + +.costHintTitle { + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.125rem; +} + +.costTable { + border-collapse: collapse; + width: 100%; + font-size: 0.8125rem; +} + +.costLabel { + color: var(--text-secondary, #555); + padding-right: 1rem; + white-space: nowrap; +} + +.costVal { + font-weight: 600; + color: var(--info-color, #1d4ed8); +} + +.costRowNeut .costLabel, +.costRowNeut .costVal { + padding-top: 0.125rem; +} + +.costRowNeut .costVal { + color: #b45309; +} + +.costHintWarn { + font-size: 0.75rem; + color: #b45309; + font-weight: 500; + line-height: 1.4; +} + +.costHintNote { + color: var(--text-secondary, #555); + font-size: 0.75rem; +} + +:global(.dark-theme) .costHint { + background: rgba(59, 130, 246, 0.08); + border-color: rgba(59, 130, 246, 0.3); +} + +:global(.dark-theme) .costVal { + color: #93c5fd; +} + +:global(.dark-theme) .costRowNeut .costVal, +:global(.dark-theme) .costHintWarn { + color: #fbbf24; +} + +/* Navigation */ +.stepNav { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: auto; + padding-top: 0.5rem; + gap: 0.75rem; +} + +.navBack { + padding: 0.5rem 1rem; + background: var(--surface-color); + color: var(--text-secondary, #666); + border: 1px solid var(--border-color, #ddd); + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.navBack:hover { + background: var(--bg-secondary, #f5f5f5); +} + +.navNext { + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0.5rem 1.25rem; + background: var(--primary-color, #f25843); + color: white; + border: none; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.navNext:hover { + background: var(--primary-dark, #d94d3a); +} + +.navConnect { + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0.625rem 1.5rem; + background: var(--primary-color, #f25843); + color: white; + border: none; + border-radius: 6px; + font-size: 0.9375rem; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; +} + +.navConnect:hover:not(:disabled) { + background: var(--primary-dark, #d94d3a); +} + +.navConnect:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Dark theme */ +:global(.dark-theme) .connectorCard { + background: var(--surface-color); +} + +:global(.dark-theme) .prefSelect, +:global(.dark-theme) .prefNumber { + background: var(--surface-color); + color: var(--text-primary); +} + +:global(.dark-theme) .summary { + border-color: var(--border-color); +} + +:global(.dark-theme) .summaryRow { + border-color: var(--border-color); +} diff --git a/src/components/AddConnectionWizard/AddConnectionWizard.tsx b/src/components/AddConnectionWizard/AddConnectionWizard.tsx new file mode 100644 index 0000000..85c9336 --- /dev/null +++ b/src/components/AddConnectionWizard/AddConnectionWizard.tsx @@ -0,0 +1,520 @@ +/** + * AddConnectionWizard + * + * Multi-step modal for adding a new connector with optional knowledge + * ingestion consent and per-connection preferences (§2.6). + * + * Steps: + * 0 — Connector wählen + * 1 — Consent (Wissensdatenbank Ja/Nein) + * 2 — Präferenzen (nur wenn Ja) + * 3 — Zusammenfassung + OAuth starten + */ + +import React, { useState } from 'react'; +import { Modal } from '../UiComponents/Modal/Modal'; +import { FaGoogle, FaMicrosoft, FaTasks, FaDatabase, FaShieldAlt, FaCheck, FaArrowRight, FaInfoCircle } from 'react-icons/fa'; +import type { KnowledgePreferences } from '../../api/connectionApi'; +import styles from './AddConnectionWizard.module.css'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export type ConnectorType = 'google' | 'msft' | 'clickup'; + +interface WizardState { + step: 0 | 1 | 2 | 3; + connector: ConnectorType | null; + knowledgeEnabled: boolean; + prefs: KnowledgePreferences; +} + +const DEFAULT_PREFS: KnowledgePreferences = { + schemaVersion: 1, + neutralizeBeforeEmbed: false, + mailContentDepth: 'full', + mailIndexAttachments: false, + filesIndexBinaries: true, + clickupScope: 'title_description', + clickupIndexAttachments: false, + maxAgeDays: 90, +}; + +const CONNECTOR_LABELS: Record = { + google: 'Google', + msft: 'Microsoft 365', + clickup: 'ClickUp', +}; + +const CONNECTOR_ICONS: Record = { + google: , + msft: , + clickup: , +}; + +// --------------------------------------------------------------------------- +// Cost estimate helper +// --------------------------------------------------------------------------- + +/** + * Returns a cost estimate broken into two lines: + * + * 1. Embedding (OpenAI text-embedding-3-small, $0.02 / 1M tokens) — always tiny. + * 2. Neutralization (Private LLM / qwen2.5 on-premise, CHF 0.01 per LLM call) + * — this is the DOMINANT cost when enabled. One call per email/task for + * short content; several calls for long threads or files. + * + * Numbers are conservative ranges. Subsequent syncs are cheaper because + * unchanged content is deduplicated before any LLM/embedding call. + */ +function computeCostEstimate( + connector: ConnectorType | null, + prefs: KnowledgePreferences, +): { + embeddingLow: string; + embeddingHigh: string; + neutralizationLow: string | null; + neutralizationHigh: string | null; + note: string; +} | null { + if (!connector) return null; + + // ---- Embedding (OpenAI, USD) ---- + const EMBED_USD_PER_M = 0.02; + const tokensPerMail: Record = { metadata: 30, snippet: 120, full: 500 }; + const depth = prefs.mailContentDepth ?? 'full'; + const maxAge = prefs.maxAgeDays ?? 90; + const mailCount = Math.min(500, Math.round((maxAge / 90) * 500)); + const taskCount = Math.min(500, Math.round((maxAge / 90) * 300)); + + let embedLowTokens = 0; + let embedHighTokens = 0; + + if (connector === 'google' || connector === 'msft') { + const mailTokens = mailCount * tokensPerMail[depth]; + embedLowTokens += mailTokens * 0.6; + embedHighTokens += mailTokens * 1.5 + 500_000; // Drive/SharePoint + if (prefs.mailIndexAttachments) embedHighTokens += 200_000; + } else if (connector === 'clickup') { + const scope = prefs.clickupScope ?? 'title_description'; + const tpt = scope === 'titles' ? 30 : scope === 'title_description' ? 200 : 400; + embedLowTokens += taskCount * tpt * 0.6; + embedHighTokens += taskCount * tpt * 1.5; + } + + const fmtUsd = (tokens: number) => { + const usd = (tokens / 1_000_000) * EMBED_USD_PER_M; + if (usd < 0.001) return '< 0.01 $'; + if (usd < 0.10) return `~${usd.toFixed(3)} $`; + return `~${usd.toFixed(2)} $`; + }; + + // ---- Neutralization (Private LLM, CHF 0.01/call) ---- + // Each item (email / task / file) = 1 LLM call for short content, + // 2-4 for long threads/documents. + const NEUT_CHF_PER_CALL = 0.01; + let neutLow: string | null = null; + let neutHigh: string | null = null; + + if (prefs.neutralizeBeforeEmbed) { + let lowCalls = 0; + let highCalls = 0; + + if (connector === 'google' || connector === 'msft') { + lowCalls += mailCount * 1; // 1 call / short email + highCalls += mailCount * 3; // up to 3 calls / long thread + lowCalls += 20; // Drive/SharePoint files (low) + highCalls += 200; // Drive/SharePoint files (high, large PDFs) + } else if (connector === 'clickup') { + lowCalls += taskCount * 1; + highCalls += taskCount * 2; + } + + const fmtChf = (calls: number) => { + const chf = calls * NEUT_CHF_PER_CALL; + if (chf < 0.01) return '< 0.01 CHF'; + return `~${chf.toFixed(2)} CHF`; + }; + + neutLow = fmtChf(lowCalls); + neutHigh = fmtChf(highCalls); + } + + return { + embeddingLow: fmtUsd(embedLowTokens), + embeddingHigh: fmtUsd(embedHighTokens), + neutralizationLow: neutLow, + neutralizationHigh: neutHigh, + note: 'Einmalig beim ersten Sync. Folge-Syncs kosten weniger (nur neue Inhalte).', + }; +} + +// --------------------------------------------------------------------------- +// Props +// --------------------------------------------------------------------------- + +interface AddConnectionWizardProps { + open: boolean; + onClose: () => void; + onConnect: ( + type: ConnectorType, + knowledgeEnabled: boolean, + prefs: KnowledgePreferences | null, + ) => Promise; + isConnecting?: boolean; +} + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +export const AddConnectionWizard: React.FC = ({ + open, + onClose, + onConnect, + isConnecting = false, +}) => { + const [state, setState] = useState({ + step: 0, + connector: null, + knowledgeEnabled: false, + prefs: { ...DEFAULT_PREFS }, + }); + + const reset = () => + setState({ step: 0, connector: null, knowledgeEnabled: false, prefs: { ...DEFAULT_PREFS } }); + + const handleClose = () => { + reset(); + onClose(); + }; + + const setStep = (step: WizardState['step']) => setState(s => ({ ...s, step })); + const setConnector = (connector: ConnectorType) => + setState(s => ({ ...s, connector, step: 1 })); + const setKnowledgeEnabled = (v: boolean) => + setState(s => ({ ...s, knowledgeEnabled: v, step: v ? 2 : 3 })); + const updatePref = (key: K, value: KnowledgePreferences[K]) => + setState(s => ({ ...s, prefs: { ...s.prefs, [key]: value } })); + + const handleConnect = async () => { + if (!state.connector) return; + await onConnect( + state.connector, + state.knowledgeEnabled, + state.knowledgeEnabled ? state.prefs : null, + ); + reset(); + onClose(); + }; + + const visibleSteps = state.knowledgeEnabled + ? [0, 1, 2, 3] + : [0, 1, 3]; + + return ( + + {/* Stepper */} +
+ {[0, 1, 2, 3].map(i => ( +
i ? styles.stepDotDone : '', + !visibleSteps.includes(i) ? styles.stepDotHidden : '', + ].join(' ')} + > + {state.step > i ? : i + 1} +
+ ))} +
+ +
+ {/* ---- Step 0: Connector ---- */} + {state.step === 0 && ( +
+

Anbieter wählen

+

Welchen Dienst möchtest du verbinden?

+
+ {(['google', 'msft', 'clickup'] as ConnectorType[]).map(type => ( + + ))} +
+
+ )} + + {/* ---- Step 1: Consent ---- */} + {state.step === 1 && ( +
+
+

Wissensdatenbank

+

+ Möchtest du Inhalte aus dieser Verbindung in deine persönliche + Wissensdatenbank aufnehmen, damit die KI beim Antworten auf Informationen + aus{' '} + {state.connector ? CONNECTOR_LABELS[state.connector] : 'diesem Dienst'}{' '} + zurückgreifen kann? +

+

+ Du kannst diese Einstellung später in den Verbindungsdetails ändern. +

+
+ + +
+
+ +
+
+ )} + + {/* ---- Step 2: Preferences ---- */} + {state.step === 2 && ( +
+

Einstellungen

+

+ Steuere, welche Inhalte und in welcher Form sie indexiert werden. +

+ +
+ +

+ Persönliche Daten (Namen, E-Mail-Adressen) werden vor dem Speichern ersetzt. +

+
+ + {(state.connector === 'google' || state.connector === 'msft') && ( + <> +
+ +
+
+ +
+ + )} + + {state.connector === 'clickup' && ( +
+ +
+ )} + +
+ +

0 = kein Limit

+
+ +
+ + +
+
+ )} + + {/* ---- Step 3: Summary ---- */} + {state.step === 3 && ( +
+

Zusammenfassung

+
+
+ Anbieter + + {CONNECTOR_ICONS[state.connector!]}  + {state.connector ? CONNECTOR_LABELS[state.connector] : '—'} + +
+
+ Wissensdatenbank + + {state.knowledgeEnabled ? '✓ Aktiv' : '✗ Nicht aktiv'} + +
+ {state.knowledgeEnabled && ( + <> +
+ Anonymisierung + + {state.prefs.neutralizeBeforeEmbed ? 'Ja' : 'Nein'} + +
+ {(state.connector === 'google' || state.connector === 'msft') && ( +
+ E-Mail-Tiefe + + {{ metadata: 'Nur Metadaten', snippet: 'Vorschautext', full: 'Volltext' }[ + state.prefs.mailContentDepth ?? 'full' + ] ?? state.prefs.mailContentDepth} + +
+ )} + {state.connector === 'clickup' && ( +
+ Aufgaben-Inhalt + + {{ + titles: 'Nur Titel', + title_description: 'Titel + Beschreibung', + with_comments: 'Titel + Beschreibung + Kommentare', + }[state.prefs.clickupScope ?? 'title_description'] ?? state.prefs.clickupScope} + +
+ )} +
+ Zeitfenster + + {state.prefs.maxAgeDays ? `${state.prefs.maxAgeDays} Tage` : 'Unbegrenzt'} + +
+ + )} +
+ + {/* Cost estimate — only shown when knowledge ingestion is enabled */} + {state.knowledgeEnabled && (() => { + const est = computeCostEstimate(state.connector, state.prefs); + if (!est) return null; + return ( +
+ +
+ Geschätzte Kosten (erster Sync) + + + + + + + {est.neutralizationLow && ( + + + + + )} + +
Embedding + {est.embeddingLow} – {est.embeddingHigh} +
Anonymisierung (Private LLM) + {est.neutralizationLow} – {est.neutralizationHigh} +
+ {est.neutralizationLow && ( + + ⚠ Anonymisierung ist der Hauptkostentreiber (CHF 0.01 pro LLM-Aufruf, on-premise). + + )} + {est.note} +
+
+ ); + })()} + +
+ + +
+
+ )} +
+
+ ); +}; + +export default AddConnectionWizard; diff --git a/src/components/FlowEditor/context/Automation2DataFlowContext.tsx b/src/components/FlowEditor/context/Automation2DataFlowContext.tsx index 8fb4419..f36f87c 100644 --- a/src/components/FlowEditor/context/Automation2DataFlowContext.tsx +++ b/src/components/FlowEditor/context/Automation2DataFlowContext.tsx @@ -6,7 +6,7 @@ import React, { createContext, useContext, useMemo } from 'react'; import type { CanvasNode, CanvasConnection } from '../editor/FlowCanvas'; import { getAvailableSources } from '../nodes/shared/dataFlowGraph'; -import type { NodeType, PortSchema, SystemVariable } from '../../../api/workflowApi'; +import type { ApiRequestFunction, FormFieldType, NodeType, PortField, PortSchema, SystemVariable } from '../../../api/workflowApi'; export interface Automation2DataFlowContextValue { currentNodeId: string; @@ -17,8 +17,15 @@ export interface Automation2DataFlowContextValue { language: string; portTypeCatalog: Record; systemVariables: Record; + /** Canonical form field types from the API — maps UI type id to portType primitive. */ + formFieldTypes: FormFieldType[]; getNodeLabel: (node: { id: string; title?: string; label?: string; type?: string }) => string; getAvailableSourceIds: () => string[]; + /** Present when rendered inside the flow editor (ConnectionPicker / tools). */ + instanceId?: string; + request?: ApiRequestFunction; + /** Build FormPayload-like schema from ``parameters[parameterKey]`` (fieldBuilder JSON). */ + parseGraphDefinedSchema: (parameterKey: string) => PortSchema | null; } const Automation2DataFlowContext = createContext(null); @@ -36,6 +43,9 @@ interface Automation2DataFlowProviderProps { language: string; portTypeCatalog?: Record; systemVariables?: Record; + formFieldTypes?: FormFieldType[]; + instanceId?: string; + request?: ApiRequestFunction; children: React.ReactNode; } @@ -48,10 +58,58 @@ export const Automation2DataFlowProvider: React.FC { const value = useMemo((): Automation2DataFlowContextValue | null => { if (!node) return null; + const formTypeToPort: Record = Object.fromEntries( + formFieldTypes.map((f) => [f.id, f.portType]) + ); + const resolvePortType = (rawType: string): string => formTypeToPort[rawType] ?? rawType; + + const parseGraphDefinedSchema = (parameterKey: string): PortSchema | null => { + const raw = node.parameters?.[parameterKey]; + if (!Array.isArray(raw)) return null; + const fields: PortField[] = []; + for (const item of raw) { + if (typeof item !== 'object' || item === null) continue; + const rec = item as Record; + if (typeof rec.name !== 'string') continue; + const lab = rec.label; + const desc = + typeof lab === 'string' ? lab : typeof lab === 'object' && lab !== null ? String((lab as Record).de ?? '') : ''; + const rawType = typeof rec.type === 'string' ? rec.type : 'str'; + if (rawType === 'group' && Array.isArray(rec.fields)) { + for (const sub of rec.fields as Record[]) { + if (!sub || typeof sub.name !== 'string') continue; + const sl = sub.label; + const sdesc = + typeof sl === 'string' + ? sl + : typeof sl === 'object' && sl !== null + ? String((sl as Record).de ?? '') + : ''; + fields.push({ + name: `${rec.name}.${sub.name}`, + type: resolvePortType(typeof sub.type === 'string' ? sub.type : 'str'), + description: (sdesc && sdesc.trim()) || `${rec.name}.${sub.name}`, + required: Boolean(sub.required), + }); + } + continue; + } + fields.push({ + name: rec.name, + type: resolvePortType(rawType), + description: (desc && desc.trim()) || rec.name, + required: Boolean(rec.required), + }); + } + return fields.length ? { name: 'FormPayload_dynamic', fields } : null; + }; return { currentNodeId: node.id, nodes, @@ -61,11 +119,15 @@ export const Automation2DataFlowProvider: React.FC n.title ?? n.label ?? n.type ?? n.id, getAvailableSourceIds: () => getAvailableSources(node.id, nodes, connections), + instanceId, + request, + parseGraphDefinedSchema, }; - }, [node?.id, nodes, connections, nodeOutputsPreview, nodeTypes, language, portTypeCatalog, systemVariables]); + }, [node, nodes, connections, nodeOutputsPreview, nodeTypes, language, portTypeCatalog, systemVariables, formFieldTypes, instanceId, request]); return ( diff --git a/src/components/FlowEditor/editor/Automation2FlowEditor.module.css b/src/components/FlowEditor/editor/Automation2FlowEditor.module.css index 5f1e938..1b32bfb 100644 --- a/src/components/FlowEditor/editor/Automation2FlowEditor.module.css +++ b/src/components/FlowEditor/editor/Automation2FlowEditor.module.css @@ -256,6 +256,225 @@ background: var(--bg-primary, #fff); } +/* Toolbar: context (load + name) is fluid with ellipsis; actions stay right-aligned. */ +.canvasHeaderRow { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: center; + width: 100%; +} + +@media (max-width: 900px) { + .canvasHeaderRow { + grid-template-columns: 1fr; + } +} + +.canvasHeaderContext { + display: flex; + align-items: center; + gap: 0.5rem; + min-width: 0; + flex: 1; +} + +/* Closed setNameValue(e.target.value)} - onBlur={_commitNameEdit} - onKeyDown={(e) => { if (e.key === 'Enter') _commitNameEdit(); if (e.key === 'Escape') setEditingName(false); }} - style={{ padding: '0.25rem 0.4rem', fontSize: '0.95rem', fontWeight: 600, border: '1px solid var(--primary-color, #007bff)', borderRadius: 4, outline: 'none', minWidth: 140, maxWidth: 300 }} - /> - ) : ( -

- {currentWorkflow.label} -

- ) - ) : ( -

- {t('Neuer Workflow')} -

- )} - {onWorkflowSettings && ( - - - {newMenuOpen && ( -
- - {onNewFromTemplate && ( - - )} -
+ )} + {targetInstanceOptions && targetInstanceOptions.length > 0 && onTargetInstanceChange && ( + )} - - - {onAutoLayout && ( - - )} - - {/* Save as template */} - {currentWorkflowId && onSaveAsTemplate && ( -
- - {templateMenuOpen && ( -
- {(['user', 'instance', 'mandate'] as const).map((s) => ( +
+
+
+ + +
+ {newMenuOpen && ( +
+ + {onNewFromTemplate && ( - ))} + )}
)}
- )} - - - {onToggleChat && ( - - )} + + {onAutoLayout && ( + + )} + + {currentWorkflowId && onSaveAsTemplate && ( +
+ + {templateMenuOpen && ( +
+ {(['user', 'instance', 'mandate'] as const).map((s) => ( + + ))} +
+ )} +
+ )} + + + {onToggleChat && ( + + )} + {_isSysAdmin && onVerboseSchemaChange && ( + + )} +
- {/* Version Selector */} {currentWorkflowId && versions && versions.length > 0 && ( -
- {t('Version:')} +
+ {t('Version:')} { const next = [...fields]; - const fieldType = e.target.value; - next[i] = { - ...next[i], - type: fieldType, - ...(fieldType === 'clickup_tasks' - ? { clickupStatusOptions: undefined } - : fieldType === 'clickup_status' - ? { clickupConnectionId: undefined, clickupListId: undefined } - : { - clickupConnectionId: undefined, - clickupListId: undefined, - clickupStatusOptions: undefined, - }), - }; + next[i] = { name: f.name, label: f.label, type: e.target.value as FormField['type'], required: f.required }; updateParam('fields', next); }} style={{ width: 'auto', minWidth: 90 }} > - - - - - - + {fieldTypeOptions.map((ft) => ( + + ))}
- {f.type === 'clickup_status' ? ( -
- {Array.isArray(f.clickupStatusOptions) && f.clickupStatusOptions.length > 0 ? ( -

- {t( - 'Dropdown mit {count} Status aus der ClickUp-Liste (Wert = exakter Status-Name für die API).', - { count: String(f.clickupStatusOptions.length) } - )} -

- ) : ( -

- {t( - 'Keine Optionen — im ClickUp-Knoten „Aufgabe erstellen“ Liste wählen und „Formular mit Liste abgleichen“.' - )} -

- )} -
- ) : null} - {f.type === 'clickup_tasks' ? ( -
- - - - { - const next = [...fields]; - next[i] = { ...next[i], clickupListId: e.target.value }; - updateParam('fields', next); - }} - style={{ width: '100%' }} - /> -

- {t('Liefert beim Ausfüllen denselben Wert wie ein ClickUp-Relationship-Feld:')}{' '} - {'{ add: [taskId], rem: [] }'}{' '} - {t('— im ClickUp-Node per Datenquelle auf das Formularfeld mappen.')} -

-
- ) : null}
))} +
+ ))} + + )} + + {entries.length === 0 && ( +
+ {t('Noch keine Quellen gewählt — wähle Daten aus vorherigen Schritten.')} +
+ )} + + + + {dataFlow && ( + setPickerOpen(false)} + onPick={addRef} + availableSourceIds={sourceIds} + nodes={dataFlow.nodes} + nodeOutputsPreview={dataFlow.nodeOutputsPreview} + getNodeLabel={dataFlow.getNodeLabel} + expectedParamType={param.type} + /> + )} + + ); +}; diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/DataRefRenderer.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/DataRefRenderer.tsx new file mode 100644 index 0000000..ecf7902 --- /dev/null +++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/DataRefRenderer.tsx @@ -0,0 +1,168 @@ +/** + * DataRefRenderer — Pick-not-Push attribute binding using the existing + * hierarchical DataPicker. + * + * For required typed parameters (e.g. ``documentList: DocumentList``) where + * the user must explicitly bind to an upstream node's typed output. Replaces + * the legacy ``frontendType: "hidden"`` so the binding becomes visible and + * editable directly in the node config panel. + */ + +import React from 'react'; +import { useLanguage } from '../../../../providers/language/LanguageContext'; +import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext'; +import { DataPicker } from '../shared/DataPicker'; +import { isRef, type DataRef, type SystemVarRef } from '../shared/dataRef'; +import type { FieldRendererProps } from './index'; + +export const DataRefRenderer: React.FC = ({ param, value, onChange }) => { + const { t } = useLanguage(); + const dataFlow = useAutomation2DataFlow(); + const [pickerOpen, setPickerOpen] = React.useState(false); + + const currentRef = isRef(value) ? (value as DataRef) : null; + const isMissing = param.required && !currentRef; + + const sourceIds = dataFlow?.getAvailableSourceIds() ?? []; + const hasSources = sourceIds.some((id) => { + const n = dataFlow?.nodes.find((x) => x.id === id); + return n?.type !== 'trigger.manual'; + }); + + const currentNodeLabel = currentRef + ? dataFlow?.getNodeLabel( + dataFlow.nodes.find((n) => n.id === currentRef.nodeId) ?? { id: currentRef.nodeId }, + ) ?? currentRef.nodeId + : null; + + const onPick = (picked: DataRef | SystemVarRef) => { + onChange(picked); + }; + + return ( +
+ + + {currentRef && ( +
+ {'\u2190'} + + {currentNodeLabel} + {currentRef.path.length > 0 && ( + <> + {' \u2192 '} + {currentRef.path.map((p) => String(p)).join('.')} + + )} + + {currentRef.expectedType && ( + + {currentRef.expectedType} + + )} + +
+ )} + + {isMissing && ( +
+ {t('Pflicht-Bindung fehlt — Quelle aus Upstream-Node wählen.')} +
+ )} + + + + {dataFlow && ( + setPickerOpen(false)} + onPick={onPick} + availableSourceIds={sourceIds} + nodes={dataFlow.nodes} + nodeOutputsPreview={dataFlow.nodeOutputsPreview} + getNodeLabel={dataFlow.getNodeLabel} + expectedParamType={param.type} + /> + )} +
+ ); +}; diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/FeatureInstancePicker.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/FeatureInstancePicker.tsx new file mode 100644 index 0000000..925e310 --- /dev/null +++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/FeatureInstancePicker.tsx @@ -0,0 +1,158 @@ +/** + * FeatureInstancePicker — renderer for frontendType="featureInstance". + * + * Modeled on ConnectionPicker. Loads mandate-scoped FeatureInstances filtered + * by `frontendOptions.featureCode` (e.g. "trustee", "redmine") via + * GET /api/workflows/{instanceId}/options/feature.instance?featureCode= + * + * Behavior matches the rest of the editor: + * - 0 results -> hint to create a feature instance for this mandate + * - 1 result -> auto-pick (no manual click required) + * - N results -> onChange(e.target.value)} + style={{ + width: '100%', + maxWidth: '100%', + boxSizing: 'border-box', + padding: '4px 8px', + borderRadius: 4, + border: '1px solid var(--border-color)', + background: 'var(--bg-primary)', + color: 'var(--text-primary)', + }} + > + + {instances.map((c) => ( + + ))} + + )} + {loadError && ( +
+ {t('Mandanten-Liste konnte nicht geladen werden')} +
+ )} + + ); +}; + +export default FeatureInstancePicker; diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/TemplateTextareaRenderer.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/TemplateTextareaRenderer.tsx new file mode 100644 index 0000000..684e14e --- /dev/null +++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/TemplateTextareaRenderer.tsx @@ -0,0 +1,171 @@ +/** + * TemplateTextarea — Freitext mit eingebetteten {{nodeId.path}} Tokens. + * Tokens werden zur Laufzeit von resolveParameterReferences aufgeloest (Gateway). + */ + +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import type { FieldRendererProps } from './index'; +import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext'; +import { DataPicker } from '../shared/DataPicker'; +import { formatRefLabel, isRef, isSystemVar, type DataRef, type SystemVarRef } from '../shared/dataRef'; +import { useLanguage } from '../../../../providers/language/LanguageContext'; +import styles from '../../editor/Automation2FlowEditor.module.css'; + +const _TEMPLATE_TOKEN_RE = /\{\{\s*([^}]+?)\s*\}\}/g; + +function _refToTemplateToken(ref: DataRef): string { + const pathSegs = (ref.path ?? []).map((p) => String(p)); + if (pathSegs.length === 0) { + return `{{${ref.nodeId}}}`; + } + return `{{${ref.nodeId}.${pathSegs.join('.')}}}`; +} + +function _insertAtCursor( + text: string, + insert: string, + start: number, + end: number, +): { next: string; caret: number } { + const next = text.slice(0, start) + insert + text.slice(end); + const caret = start + insert.length; + return { next, caret }; +} + +function _parseTokensInTemplate( + template: string, + nodes: Array<{ id: string; title?: string }>, + getNodeLabel: (n: { id: string; title?: string }) => string, +): Array<{ raw: string; label: string }> { + const out: Array<{ raw: string; label: string }> = []; + const seen = new Set(); + let m: RegExpExecArray | null; + const re = new RegExp(_TEMPLATE_TOKEN_RE.source, 'g'); + while ((m = re.exec(template)) !== null) { + const inner = m[1].trim(); + if (seen.has(inner)) continue; + seen.add(inner); + const parts = inner.split('.'); + const nodeId = parts[0]; + if (!nodeId) continue; + const path = parts.slice(1).map((seg) => (/^\d+$/.test(seg) ? parseInt(seg, 10) : seg)); + const ref: DataRef = { type: 'ref', nodeId, path }; + const label = formatRefLabel(ref, nodes, (id) => + getNodeLabel(nodes.find((n) => n.id === id) ?? { id }), + ); + out.push({ raw: m[0], label }); + } + return out; +} + +export const TemplateTextareaRenderer: React.FC = ({ param, value, onChange }) => { + const { t } = useLanguage(); + const dataFlow = useAutomation2DataFlow(); + const textareaRef = useRef(null); + const [pickerOpen, setPickerOpen] = useState(false); + + const strVal = typeof value === 'string' ? value : value != null ? String(value) : ''; + + const sourceIds = dataFlow?.getAvailableSourceIds() ?? []; + const hasSources = sourceIds.some((id) => { + const n = dataFlow?.nodes.find((x) => x.id === id); + return n?.type !== 'trigger.manual'; + }); + + const tokenLegend = useMemo(() => { + if (!dataFlow || !strVal.includes('{{')) return []; + return _parseTokensInTemplate(strVal, dataFlow.nodes, dataFlow.getNodeLabel); + }, [strVal, dataFlow]); + + const handlePick = useCallback( + (picked: DataRef | SystemVarRef) => { + if (isSystemVar(picked)) { + setPickerOpen(false); + return; + } + if (!isRef(picked)) { + setPickerOpen(false); + return; + } + const token = _refToTemplateToken(picked); + const el = textareaRef.current; + const start = el?.selectionStart ?? strVal.length; + const end = el?.selectionEnd ?? strVal.length; + const { next, caret } = _insertAtCursor(strVal, token, start, end); + onChange(next); + setPickerOpen(false); + requestAnimationFrame(() => { + const ta = textareaRef.current; + if (ta) { + ta.focus(); + ta.setSelectionRange(caret, caret); + } + }); + }, + [onChange, strVal], + ); + + return ( +
+ +
+ + {!hasSources && ( + {t('Keine vorherigen Nodes verfügbar')} + )} +
+