Merge branch 'master' into ocid_well_known

This commit is contained in:
einhornimmond 2025-07-29 11:32:31 +02:00 committed by GitHub
commit a479a6b7ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1852 additions and 33120 deletions

View File

@ -4,8 +4,23 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [2.6.0](https://github.com/gradido/gradido/compare/2.3.1...2.6.0)
#### [2.6.1](https://github.com/gradido/gradido/compare/2.3.1...2.6.1)
- refactor(frontend): transaction and contribution form [`#3519`](https://github.com/gradido/gradido/pull/3519)
- fix(federation): fix some attack vectors in communities handshake [`#3517`](https://github.com/gradido/gradido/pull/3517)
- fix(other): start sh when called from webhook [`#3515`](https://github.com/gradido/gradido/pull/3515)
- feat(backend): introduce encrypted jwts in backend federation communication [`#3510`](https://github.com/gradido/gradido/pull/3510)
- feat(other): write playwright tests [`#3509`](https://github.com/gradido/gradido/pull/3509)
- feat(frontend): keep branding project in browser url bar [`#3512`](https://github.com/gradido/gradido/pull/3512)
- feat(other): add clear command for yarn and turbo [`#3513`](https://github.com/gradido/gradido/pull/3513)
- fix(other): in deploy run only core count tasks with turbo at the same time [`#3511`](https://github.com/gradido/gradido/pull/3511)
- refactor(federation): move code for checking pending transactions [`#3508`](https://github.com/gradido/gradido/pull/3508)
- refactor(other): add shared module [`#3507`](https://github.com/gradido/gradido/pull/3507)
- refactor(other): centralize logging code, use log4js config-generator [`#3506`](https://github.com/gradido/gradido/pull/3506)
- fix(frontend): fix password labels [`#3504`](https://github.com/gradido/gradido/pull/3504)
- fix(backend): update contribution frontend link [`#3502`](https://github.com/gradido/gradido/pull/3502)
- refactor(database): move database connection into database module [`#3503`](https://github.com/gradido/gradido/pull/3503)
- chore(release): v2.6.0 beta [`#3501`](https://github.com/gradido/gradido/pull/3501)
- fix(frontend): fix contribution link [`#3500`](https://github.com/gradido/gradido/pull/3500)
- feat(other): disable index html caching, reenable limits [`#3497`](https://github.com/gradido/gradido/pull/3497)
- fix(admin): fix accidently remove user states [`#3496`](https://github.com/gradido/gradido/pull/3496)

View File

@ -3,7 +3,7 @@
"description": "Administration Interface for Gradido",
"main": "index.js",
"author": "Gradido Academy - https://www.gradido.net",
"version": "2.6.0",
"version": "2.6.1",
"license": "Apache-2.0",
"scripts": {
"dev": "vite",

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "backend",
"version": "2.6.0",
"version": "2.6.1",
"private": false,
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"repository": "https://github.com/gradido/gradido/backend",

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@
},
"admin": {
"name": "admin",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"@iconify/json": "^2.2.228",
"@popperjs/core": "^2.11.8",
@ -85,7 +85,7 @@
},
"backend": {
"name": "backend",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"cross-env": "^7.0.3",
"email-templates": "^10.0.1",
@ -162,7 +162,7 @@
},
"config-schema": {
"name": "config-schema",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"esbuild": "^0.25.2",
"joi": "^17.13.3",
@ -180,22 +180,25 @@
},
"core": {
"name": "core",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"database": "*",
"esbuild": "^0.25.2",
"jose": "^4.14.4",
"log4js": "^6.9.1",
"shared": "*",
"zod": "^3.25.61",
},
"devDependencies": {
"@biomejs/biome": "2.0.0",
"@types/node": "^17.0.21",
"type-graphql": "^1.1.1",
"typescript": "^4.9.5",
},
},
"database": {
"name": "database",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"@types/uuid": "^8.3.4",
"cross-env": "^7.0.3",
@ -230,12 +233,12 @@
"ts-jest": "27.0.5",
"ts-node": "^10.9.2",
"typescript": "^4.9.5",
"vitest": "^3.2.4",
"vitest": "^2.0.5",
},
},
"dht-node": {
"name": "dht-node",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"cross-env": "^7.0.3",
"dht-rpc": "6.18.1",
@ -273,7 +276,7 @@
},
"federation": {
"name": "federation",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"cross-env": "^7.0.3",
"sodium-native": "^3.4.1",
@ -295,6 +298,7 @@
"await-semaphore": "0.1.3",
"class-validator": "^0.13.2",
"config-schema": "*",
"core": "*",
"cors": "2.8.5",
"database": "*",
"decimal.js-light": "^2.5.1",
@ -324,7 +328,7 @@
},
"frontend": {
"name": "frontend",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"@morev/vue-transitions": "^3.0.2",
"@types/leaflet": "^1.9.12",
@ -419,10 +423,11 @@
},
"shared": {
"name": "shared",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"decimal.js-light": "^2.5.1",
"esbuild": "^0.25.2",
"jose": "^4.14.4",
"log4js": "^6.9.1",
"zod": "^3.25.61",
},
@ -1006,8 +1011,6 @@
"@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="],
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/content-disposition": ["@types/content-disposition@0.5.8", "", {}, "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg=="],
@ -1016,8 +1019,6 @@
"@types/cors": ["@types/cors@2.8.10", "", {}, "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/dotenv": ["@types/dotenv@8.2.3", "", { "dependencies": { "dotenv": "*" } }, "sha512-g2FXjlDX/cYuc5CiQvyU/6kkbP1JtmGzh0obW50zD7OKeILVL0NSpPWLXVfqoAGQjom2/SLLx9zHq0KXvD6mbw=="],
"@types/email-templates": ["@types/email-templates@10.0.4", "", { "dependencies": { "@types/html-to-text": "*", "@types/nodemailer": "*", "juice": "^8.0.0" } }, "sha512-8O2bdGPO6RYgH2DrnFAcuV++s+8KNA5e2Erjl6UxgKRVsBH9zXu2YLrLyOBRMn2VyEYmzgF+6QQUslpVhj0y/g=="],
@ -2702,7 +2703,7 @@
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
@ -3112,7 +3113,7 @@
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
"tinypool": ["tinypool@1.0.2", "", {}, "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA=="],
@ -3528,10 +3529,10 @@
"@nuxt/kit/pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="],
"@nuxt/kit/tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"@rollup/pluginutils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"@selderee/plugin-htmlparser2/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="],
"@swc/cli/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
@ -3598,8 +3599,6 @@
"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"apollo-boost/ts-invariant": ["ts-invariant@0.4.4", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA=="],
"apollo-boost/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
@ -3692,8 +3691,6 @@
"database/ts-jest": ["ts-jest@27.0.5", "", { "dependencies": { "bs-logger": "0.x", "fast-json-stable-stringify": "2.x", "jest-util": "^27.0.0", "json5": "2.x", "lodash": "4.x", "make-error": "1.x", "semver": "7.x", "yargs-parser": "20.x" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@types/jest": "^27.0.0", "babel-jest": ">=27.0.0 <28", "jest": "^27.0.0", "typescript": ">=3.8 <5.0" }, "optionalPeers": ["@babel/core", "@types/jest", "babel-jest"], "bin": { "ts-jest": "cli.js" } }, "sha512-lIJApzfTaSSbtlksfFNHkWOzLJuuSm4faFAfo5kvzOiRAuoN4/eKxVJ2zEAho8aecE04qX6K1pAzfH5QHL1/8w=="],
"database/vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
"decompress-response/mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"dht-node/@types/jest": ["@types/jest@27.5.1", "", { "dependencies": { "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" } }, "sha512-fUy7YRpT+rHXto1YlL+J9rs0uLGyiqVt3ZOTQR+4ROc47yNl8WLdVLgUloBRhOxP1PZvguHl44T3H0wAWxahYQ=="],
@ -3742,6 +3739,8 @@
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"fdir/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"federation/apollo-server-testing": ["apollo-server-testing@2.25.2", "", { "dependencies": { "apollo-server-core": "^2.25.2" }, "peerDependencies": { "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" } }, "sha512-HjQV9wPbi/ZqpRbyyhNwCbaDnfjDM0hTRec5TOoOjurEZ/vh4hTPHwGkDZx3kbcWowhGxe2qoHM6KANSB/SxuA=="],
"federation/helmet": ["helmet@7.2.0", "", {}, "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw=="],
@ -3820,8 +3819,6 @@
"jest-util/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="],
"jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"jest-watcher/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="],
"jest-worker/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="],
@ -3844,8 +3841,6 @@
"mailparser/tlds": ["tlds@1.255.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"multimatch/@types/minimatch": ["@types/minimatch@3.0.5", "", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="],
@ -3962,6 +3957,8 @@
"test-exclude/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"ts-jest/jest": ["jest@27.5.1", "", { "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", "jest-cli": "^27.5.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ=="],
"ts-jest/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
@ -3992,14 +3989,16 @@
"unimport/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"unimport/pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="],
"unimport/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"unimport/tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
"unimport/pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="],
"unimport/unplugin": ["unplugin@2.3.2", "", { "dependencies": { "acorn": "^8.14.1", "picomatch": "^4.0.2", "webpack-virtual-modules": "^0.6.2" } }, "sha512-3n7YA46rROb3zSj8fFxtxC/PqoyvYQ0llwz9wtUPUutr9ig09C8gGo5CWCwHrUzlqC1LLR43kxp5vEIyH1ac1w=="],
"unplugin-utils/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"unplugin-utils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"unplugin-vue-components/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"unplugin-vue-components/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@ -4122,30 +4121,6 @@
"database/ts-jest/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
"database/vitest/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
"database/vitest/@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
"database/vitest/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
"database/vitest/@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
"database/vitest/@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
"database/vitest/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
"database/vitest/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
"database/vitest/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"database/vitest/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"database/vitest/tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
"database/vitest/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
"database/vitest/vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
"dht-node/ts-jest/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
"dht-rpc/sodium-universal/sodium-native": ["sodium-native@5.0.1", "", { "dependencies": { "require-addon": "^1.1.0", "which-runtime": "^1.2.1" } }, "sha512-Q305aUXc0OzK7VVRvWkeEQJQIHs6slhFwWpyqLB5iJqhpyt2lYIVu96Y6PQ7TABIlWXVF3YiWDU3xS2Snkus+g=="],
@ -4216,8 +4191,6 @@
"jest-worker/jest-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="],
"jest-worker/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"js-beautify/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"jsdom/parse5/entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="],
@ -4274,6 +4247,8 @@
"typeorm/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"unctx/unplugin/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"unimport/pkg-types/confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
"unplugin-vue-components/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@ -4282,8 +4257,6 @@
"unplugin-vue-components/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"vite-plugin-html/@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
@ -4360,8 +4333,6 @@
"cheerio-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
"chokidar-cli/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"chokidar-cli/yargs/cliui/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="],
"chokidar-cli/yargs/cliui/wrap-ansi": ["wrap-ansi@5.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", "strip-ansi": "^5.0.0" } }, "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q=="],
@ -4378,12 +4349,6 @@
"css-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
"database/vitest/@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"database/vitest/@vitest/spy/tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="],
"database/vitest/@vitest/utils/loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="],
"editorconfig/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
@ -4408,8 +4373,6 @@
"mailparser/html-to-text/selderee/parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="],
"nodemon/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"run-applescript/execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="],
@ -4426,8 +4389,6 @@
"typeorm/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"unplugin-vue-components/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"unplugin-vue-components/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"vue-apollo/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],

View File

@ -1,6 +1,6 @@
{
"name": "config-schema",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido Config for validate config",
"main": "./build/index.js",
"types": "./src/index.ts",

View File

@ -1,6 +1,6 @@
{
"name": "core",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido Core Code, High-Level Shared Code, with dependencies on other modules",
"main": "./build/index.js",
"types": "./src/index.ts",

View File

@ -1,6 +1,6 @@
{
"name": "database",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido Database Tool to execute database migrations",
"main": "./build/index.js",
"types": "./src/index.ts",
@ -46,7 +46,7 @@
"ts-jest": "27.0.5",
"ts-node": "^10.9.2",
"typescript": "^4.9.5",
"vitest": "^3.2.4"
"vitest": "^2.0.5"
},
"dependencies": {
"@types/uuid": "^8.3.4",

View File

@ -1,6 +1,6 @@
{
"name": "dht-node",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido dht-node module",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/",

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "gradido-dlt-connector",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido DLT-Connector",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/",

View File

@ -1,6 +1,6 @@
{
"name": "federation",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/federation",

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "2.6.0",
"version": "2.6.1",
"private": true,
"scripts": {
"dev": "concurrently \"yarn watch-scss\" \"vite\"",
@ -82,7 +82,7 @@
"@vue/test-utils": "^2.4.6",
"chokidar-cli": "^3.0.0",
"concurrently": "^9.1.2",
"config-schema": "2.6.0",
"config-schema": "*",
"cross-env": "^7.0.3",
"dotenv-webpack": "^7.0.3",
"eslint": "8.57.1",

View File

@ -22,7 +22,7 @@
</template>
<script setup>
import { ref, computed, watch, onMounted, onUpdated } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { useRoute } from 'vue-router'
import { selectCommunities } from '@/graphql/queries'
@ -50,6 +50,9 @@ onResult(({ data }) => {
if (data) {
communities.value = data.communities
setDefaultCommunity()
if (data.communities.length === 1) {
validCommunityIdentifier.value = true
}
}
})

View File

@ -2,24 +2,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import ContributionForm from './ContributionForm.vue'
// Mock external components and dependencies
vi.mock('@/components/Inputs/InputAmount', () => ({
default: {
name: 'InputAmount',
template: '<input data-testid="input-amount" />',
},
}))
vi.mock('@/components/Inputs/InputTextarea', () => ({
default: {
name: 'InputTextarea',
template: '<textarea data-testid="input-textarea"></textarea>',
},
}))
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key,
d: (date) => date,
}),
}))

View File

@ -19,6 +19,7 @@
class="mb-4 bg-248"
type="date"
:rules="validationSchema.fields.contributionDate"
:disable-smart-valid-state="disableSmartValidState"
@update:model-value="updateField"
/>
<div v-if="noOpenCreation" class="p-3" data-test="contribution-message">
@ -33,6 +34,7 @@
:placeholder="$t('contribution.yourActivity')"
:rules="validationSchema.fields.memo"
textarea="true"
:disable-smart-valid-state="disableSmartValidState"
@update:model-value="updateField"
/>
<ValidatedInput
@ -41,8 +43,9 @@
:label="$t('form.hours')"
placeholder="0.01"
step="0.01"
type="number"
type="text"
:rules="validationSchema.fields.hours"
:disable-smart-valid-state="disableSmartValidState"
@update:model-value="updateField"
/>
<LabeledInput
@ -68,7 +71,7 @@
{{ $t('form.cancel') }}
</BButton>
</BCol>
<BCol class="text-end mt-lg-0">
<BCol class="text-end mt-lg-0" @mouseover="disableSmartValidState = true">
<BButton
block
type="submit"
@ -89,9 +92,8 @@ import { reactive, computed, ref, onMounted, onUnmounted, toRaw } from 'vue'
import { useI18n } from 'vue-i18n'
import ValidatedInput from '@/components/Inputs/ValidatedInput'
import LabeledInput from '@/components/Inputs/LabeledInput'
import { memo as memoSchema } from '@/validationSchemas'
import OpenCreationsAmount from './OpenCreationsAmount.vue'
import { object, date as dateSchema, number } from 'yup'
import { object, date as dateSchema, number, string } from 'yup'
import { GDD_PER_HOUR } from '../../constants'
const amountToHours = (amount) => parseFloat(amount / GDD_PER_HOUR).toFixed(2)
@ -105,7 +107,7 @@ const props = defineProps({
const emit = defineEmits(['upsert-contribution', 'abort'])
const { t } = useI18n()
const { t, d } = useI18n()
const entityDataToForm = computed(() => ({
...props.modelValue,
@ -121,6 +123,7 @@ const entityDataToForm = computed(() => ({
const form = reactive({ ...entityDataToForm.value })
const now = ref(new Date()) // checked every minute, updated if day, month or year changed
const disableSmartValidState = ref(false)
const isThisMonth = computed(() => {
const formContributionDate = new Date(form.contributionDate)
@ -147,16 +150,26 @@ const validationSchema = computed(() => {
// The date field is required and needs to be a valid date
// contribution date
contributionDate: dateSchema()
.required()
.min(minimalDate.value.toISOString().slice(0, 10)) // min date is first day of last month
.max(now.value.toISOString().slice(0, 10)), // date cannot be in the future
memo: memoSchema,
.required('form.validation.contributionDate.required')
.min(minimalDate.value.toISOString().slice(0, 10), ({ min }) => ({
key: 'form.validation.contributionDate.min',
values: { min: d(min) },
})) // min date is first day of last month
.max(now.value.toISOString().slice(0, 10), ({ max }) => ({
key: 'form.validation.contributionDate.max',
values: { max: d(max) },
})), // date cannot be in the future
memo: string()
.min(5, ({ min }) => ({ key: 'form.validation.contributionMemo.min', values: { min } }))
.max(255, ({ max }) => ({ key: 'form.validation.contributionMemo.max', values: { max } }))
.required('form.validation.contributionMemo.required'),
hours: number()
.typeError({ key: 'form.validation.hours.typeError', values: { min: 0.01, max: maxHours } })
.required()
.transform((value, originalValue) => (originalValue === '' ? undefined : value))
.min(0.01, ({ min }) => ({ key: 'form.validation.gddCreationTime.min', values: { min } }))
.max(maxHours, ({ max }) => ({ key: 'form.validation.gddCreationTime.max', values: { max } }))
.test('decimal-places', 'form.validation.gddCreationTime.decimal-places', (value) => {
// .transform((value, originalValue) => (originalValue === '' ? undefined : value))
.min(0.01, ({ min }) => ({ key: 'form.validation.hours.min', values: { min } }))
.max(maxHours, ({ max }) => ({ key: 'form.validation.hours.max', values: { max } }))
.test('decimal-places', 'form.validation.hours.decimal-places', (value) => {
if (value === undefined || value === null) return true
return /^\d+(\.\d{0,2})?$/.test(value.toString())
}),

View File

@ -3,8 +3,16 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import TransactionForm from './TransactionForm'
import { nextTick, ref } from 'vue'
import { SEND_TYPES } from '@/utils/sendTypes'
import { BCard, BForm, BFormRadioGroup, BRow, BCol, BFormRadio, BButton } from 'bootstrap-vue-next'
import { useForm } from 'vee-validate'
import {
BCard,
BForm,
BFormRadioGroup,
BRow,
BCol,
BFormRadio,
BButton,
BFormInvalidFeedback,
} from 'bootstrap-vue-next'
import { useRoute } from 'vue-router'
vi.mock('vue-router', () => ({
@ -35,23 +43,6 @@ vi.mock('@/composables/useToast', () => ({
})),
}))
vi.mock('vee-validate', () => {
const actualUseForm = vi.fn().mockReturnValue({
handleSubmit: vi.fn((callback) => {
return () =>
callback({
identifier: 'test@example.com',
amount: '100,00',
memo: 'Test memo',
})
}),
resetForm: vi.fn(),
defineField: vi.fn(() => [vi.fn(), {}]),
})
return { useForm: actualUseForm }
})
describe('TransactionForm', () => {
let wrapper
@ -64,6 +55,9 @@ describe('TransactionForm', () => {
mocks: {
$t: mockT,
$n: mockN,
$i18n: {
locale: 'en',
},
},
components: {
BCard,
@ -73,12 +67,11 @@ describe('TransactionForm', () => {
BCol,
BFormRadio,
BButton,
BFormInvalidFeedback,
},
stubs: {
'community-switch': true,
'input-identifier': true,
'input-amount': true,
'input-textarea': true,
'validated-input': true,
},
},
props: {
@ -102,15 +95,15 @@ describe('TransactionForm', () => {
describe('with balance <= 0.00 GDD the form is disabled', () => {
it('has a disabled input field of type text', () => {
expect(wrapper.find('input-identifier-stub').attributes('disabled')).toBe('true')
expect(wrapper.find('#identifier').attributes('disabled')).toBe('true')
})
it('has a disabled input field for amount', () => {
expect(wrapper.find('input-amount-stub').attributes('disabled')).toBe('true')
expect(wrapper.find('#amount').attributes('disabled')).toBe('true')
})
it('has a disabled textarea field', () => {
expect(wrapper.find('input-textarea-stub').attributes('disabled')).toBe('true')
expect(wrapper.find('#memo').attributes('disabled')).toBe('true')
})
it('has a message indicating that there are no GDDs to send', () => {
@ -143,41 +136,39 @@ describe('TransactionForm', () => {
describe('identifier field', () => {
it('has an input field of type text', () => {
expect(wrapper.find('input-identifier-stub').exists()).toBe(true)
expect(wrapper.find('#identifier').exists()).toBe(true)
})
it('has a label form.recipient', () => {
expect(wrapper.find('input-identifier-stub').attributes('label')).toBe('form.recipient')
expect(wrapper.find('#identifier').attributes('label')).toBe('form.recipient')
})
it('has a placeholder for identifier', () => {
expect(wrapper.find('input-identifier-stub').attributes('placeholder')).toBe(
'form.identifier',
)
expect(wrapper.find('#identifier').attributes('placeholder')).toBe('form.identifier')
})
})
describe('amount field', () => {
it('has an input field of type text', () => {
expect(wrapper.find('input-amount-stub').exists()).toBe(true)
expect(wrapper.find('#amount').exists()).toBe(true)
})
it('has a label form.amount', () => {
expect(wrapper.find('input-amount-stub').attributes('label')).toBe('form.amount')
expect(wrapper.find('#amount').attributes('label')).toBe('form.amount')
})
it('has a placeholder "0.01"', () => {
expect(wrapper.find('input-amount-stub').attributes('placeholder')).toBe('0.01')
expect(wrapper.find('#amount').attributes('placeholder')).toBe('0.01')
})
})
describe('message text box', () => {
it('has a textarea field', () => {
expect(wrapper.find('input-textarea-stub').exists()).toBe(true)
expect(wrapper.find('#memo').exists()).toBe(true)
})
it('has a label form.message', () => {
expect(wrapper.find('input-textarea-stub').attributes('label')).toBe('form.message')
expect(wrapper.find('#memo').attributes('label')).toBe('form.message')
})
})
@ -233,8 +224,10 @@ describe('TransactionForm', () => {
})
it('emits set-transaction event with correct data when form is submitted', async () => {
wrapper.vm.form.identifier = 'test@example.com'
wrapper.vm.form.amount = '100,00'
wrapper.vm.form.memo = 'Test memo'
await wrapper.findComponent(BForm).trigger('submit.prevent')
expect(wrapper.emitted('set-transaction')).toBeTruthy()
expect(wrapper.emitted('set-transaction')[0][0]).toEqual(
expect.objectContaining({
@ -247,20 +240,10 @@ describe('TransactionForm', () => {
})
it('handles form submission with empty amount', async () => {
vi.mocked(useForm).mockReturnValueOnce({
...vi.mocked(useForm)(),
handleSubmit: vi.fn((callback) => {
return () =>
callback({
identifier: 'test@example.com',
amount: '',
memo: 'Test memo',
})
}),
})
wrapper = createWrapper({ balance: 100.0 })
await nextTick()
wrapper.vm.form.identifier = 'test@example.com'
wrapper.vm.form.memo = 'Test memo'
await wrapper.findComponent(BForm).trigger('submit.prevent')
expect(wrapper.emitted('set-transaction')).toBeTruthy()

View File

@ -46,20 +46,25 @@
<BRow>
<BCol class="fw-bold">
<community-switch
:disabled="isBalanceDisabled"
:model-value="targetCommunity"
@update:model-value="targetCommunity = $event"
:disabled="isBalanceEmpty"
:model-value="form.targetCommunity"
@update:model-value="updateField($event, 'targetCommunity')"
/>
</BCol>
</BRow>
</BCol>
<BCol v-if="radioSelected === SEND_TYPES.send" cols="12">
<div v-if="!userIdentifier">
<input-identifier
<ValidatedInput
id="identifier"
:model-value="form.identifier"
name="identifier"
:label="$t('form.recipient')"
:placeholder="$t('form.identifier')"
:disabled="isBalanceDisabled"
:rules="validationSchema.fields.identifier"
:disabled="isBalanceEmpty"
:disable-smart-valid-state="disableSmartValidState"
@update:model-value="updateField"
/>
</div>
<div v-else class="mb-4">
@ -72,13 +77,17 @@
</div>
</BCol>
<BCol cols="12" lg="6">
<input-amount
<ValidatedInput
id="amount"
:model-value="form.amount"
name="amount"
:label="$t('form.amount')"
:placeholder="'0.01'"
:rules="{ required: true, gddSendAmount: { min: 0.01, max: balance } }"
:disabled="isBalanceDisabled"
></input-amount>
:rules="validationSchema.fields.amount"
:disabled="isBalanceEmpty"
:disable-smart-valid-state="disableSmartValidState"
@update:model-value="updateField"
/>
</BCol>
</BRow>
</BCol>
@ -86,16 +95,21 @@
<BRow>
<BCol>
<input-textarea
<ValidatedInput
id="memo"
:model-value="form.memo"
name="memo"
:label="$t('form.message')"
:placeholder="$t('form.message')"
:rules="{ required: true, min: 5, max: 255 }"
:disabled="isBalanceDisabled"
:rules="validationSchema.fields.memo"
textarea="true"
:disabled="isBalanceEmpty"
:disable-smart-valid-state="disableSmartValidState"
@update:model-value="updateField"
/>
</BCol>
</BRow>
<div v-if="!!isBalanceDisabled" class="text-danger mt-5">
<div v-if="!!isBalanceEmpty" class="text-danger mt-5">
{{ $t('form.no_gdd_available') }}
</div>
<BRow v-else class="test-buttons mt-3">
@ -110,8 +124,14 @@
{{ $t('form.reset') }}
</BButton>
</BCol>
<BCol cols="12" md="6" lg="6" class="text-lg-end">
<BButton block type="submit" variant="gradido">
<BCol
cols="12"
md="6"
lg="6"
class="text-lg-end"
@mouseover="disableSmartValidState = true"
>
<BButton block type="submit" variant="gradido" :disabled="formIsInvalid">
{{ $t('form.check_now') }}
</BButton>
</BCol>
@ -124,15 +144,14 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useQuery } from '@vue/apollo-composable'
import { useForm } from 'vee-validate'
import { SEND_TYPES } from '@/utils/sendTypes'
import InputIdentifier from '@/components/Inputs/InputIdentifier'
import InputAmount from '@/components/Inputs/InputAmount'
import InputTextarea from '@/components/Inputs/InputTextarea'
import CommunitySwitch from '@/components/CommunitySwitch.vue'
import ValidatedInput from '@/components/Inputs/ValidatedInput.vue'
import { memo as memoSchema, identifier as identifierSchema } from '@/validationSchemas'
import { object, number } from 'yup'
import { user } from '@/graphql/queries'
import CONFIG from '@/config'
import { useAppToast } from '@/composables/useToast'
@ -149,6 +168,10 @@ const props = defineProps({
},
})
const entityDataToForm = computed(() => ({ ...props }))
const form = reactive({ ...entityDataToForm.value })
const disableSmartValidState = ref(false)
const emit = defineEmits(['set-transaction'])
const route = useRoute()
@ -157,18 +180,6 @@ const { toastError } = useAppToast()
const radioSelected = ref(props.selected)
const userName = ref('')
const recipientCommunity = ref({ uuid: '', name: '' })
const { handleSubmit, resetForm, defineField, values } = useForm({
initialValues: {
identifier: props.identifier,
amount: props.amount ? String(props.amount) : '',
memo: props.memo,
targetCommunity: props.targetCommunity,
},
})
const [targetCommunity, targetCommunityProps] = defineField('targetCommunity')
const userIdentifier = computed(() => {
if (route.params.userIdentifier && route.params.communityIdentifier) {
@ -180,7 +191,49 @@ const userIdentifier = computed(() => {
return null
})
const isBalanceDisabled = computed(() => props.balance <= 0)
const validationSchema = computed(() => {
const amountSchema = number()
.required()
.typeError({
key: 'form.validation.amount.typeError',
values: { min: 0.01, max: props.balance },
})
.transform((value, originalValue) => {
if (typeof originalValue === 'string') {
return Number(originalValue.replace(',', '.'))
}
return value
})
.min(0.01, ({ min }) => ({ key: 'form.validation.amount.min', values: { min } }))
.max(props.balance, ({ max }) => ({ key: 'form.validation.amount.max', values: { max } }))
.test('decimal-places', 'form.validation.amount.decimal-places', (value) => {
if (value === undefined || value === null) return true
return /^\d+(\.\d{0,2})?$/.test(value.toString())
})
if (!userIdentifier.value && radioSelected.value === SEND_TYPES.send) {
return object({
memo: memoSchema,
amount: amountSchema,
identifier: identifierSchema,
})
} else {
// don't need identifier schema if it is a transaction link or identifier was set via url
return object({
memo: memoSchema,
amount: amountSchema,
})
}
})
const formIsInvalid = computed(() => !validationSchema.value.isValidSync(form))
const updateField = (newValue, name) => {
if (typeof name === 'string' && name.length) {
form[name] = newValue
}
}
const isBalanceEmpty = computed(() => props.balance <= 0)
const { result: userResult, error: userError } = useQuery(
user,
@ -193,6 +246,7 @@ watch(
(user) => {
if (user) {
userName.value = `${user.firstName} ${user.lastName}`
form.identifier = userIdentifier.value.identifier
}
},
{ immediate: true },
@ -204,19 +258,21 @@ watch(userError, (error) => {
}
})
const onSubmit = handleSubmit((formValues) => {
if (userIdentifier.value) formValues.identifier = userIdentifier.value.identifier
function onSubmit() {
const transformedForm = validationSchema.value.cast(form)
emit('set-transaction', {
...transformedForm,
selected: radioSelected.value,
...formValues,
amount: Number(formValues.amount.replace(',', '.')),
userName: userName.value,
})
})
}
function onReset(event) {
event.preventDefault()
resetForm()
form.amount = props.amount
form.memo = props.memo
form.identifier = props.identifier
form.targetCommunity = props.targetCommunity
radioSelected.value = SEND_TYPES.send
router.replace('/send')
}

View File

@ -1,125 +0,0 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import InputAmount from './InputAmount'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ref } from 'vue'
import { BFormInput } from 'bootstrap-vue-next'
vi.mock('vue-router', () => ({
useRoute: vi.fn(() => ({
params: {},
path: '/some-path',
})),
}))
vi.mock('vue-i18n', () => ({
useI18n: vi.fn(() => ({
t: (key) => key,
n: (num) => num,
})),
}))
vi.mock('vee-validate', () => ({
useField: vi.fn(() => ({
value: ref(''),
meta: { valid: true },
errorMessage: ref(''),
})),
}))
// Mock toast
const mockToastError = vi.fn()
vi.mock('@/composables/useToast', () => ({
useAppToast: vi.fn(() => ({
toastError: mockToastError,
})),
}))
describe('InputAmount', () => {
let wrapper
const createWrapper = (propsData = {}) => {
return mount(InputAmount, {
props: {
name: 'amount',
label: 'Amount',
placeholder: 'Enter amount',
typ: 'TransactionForm',
modelValue: '12,34',
...propsData,
},
global: {
mocks: {
$route: useRoute(),
...useI18n(),
},
components: {
BFormInput,
},
directives: {
focus: {},
},
stubs: {
BFormGroup: true,
BFormInvalidFeedback: true,
BInputGroup: true,
},
},
})
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('mount in a TransactionForm', () => {
beforeEach(() => {
wrapper = createWrapper()
})
it('renders the component input-amount', () => {
expect(wrapper.find('div.input-amount').exists()).toBe(true)
})
it('normalizes the amount correctly', async () => {
await wrapper.vm.normalizeAmount('12,34')
expect(wrapper.vm.value).toBe('12.34')
})
it('does not normalize invalid input', async () => {
await wrapper.vm.normalizeAmount('12m34')
expect(wrapper.vm.value).toBe('12m34')
})
})
describe('mount in a ContributionForm', () => {
beforeEach(() => {
wrapper = createWrapper({
typ: 'ContributionForm',
modelValue: '12.34',
})
})
it('renders the component input-amount', () => {
expect(wrapper.find('div.input-amount').exists()).toBe(true)
})
it('normalizes the amount correctly', async () => {
await wrapper.vm.normalizeAmount('12.34')
expect(wrapper.vm.value).toBe('12.34')
})
it('does not normalize invalid input', async () => {
await wrapper.vm.normalizeAmount('12m34')
expect(wrapper.vm.value).toBe('12m34')
})
})
it('emits update:modelValue when value changes', async () => {
wrapper = createWrapper()
await wrapper.vm.normalizeAmount('15.67')
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['15.67'])
})
})

View File

@ -1,88 +0,0 @@
<template>
<div class="input-amount">
<template v-if="typ === 'TransactionForm'">
<BFormGroup :label="label" :label-for="labelFor" data-test="input-amount">
<BFormInput
:id="labelFor"
v-focus="amountFocused"
:model-value="value"
:class="$route.path === '/send' ? 'bg-248' : ''"
:name="name"
:placeholder="placeholder"
type="text"
:state="meta.valid"
trim
:disabled="disabled"
autocomplete="off"
@update:model-value="normalizeAmount($event)"
@focus="amountFocused = true"
@blur="normalizeAmount($event)"
/>
<BFormInvalidFeedback v-if="errorMessage">
{{ errorMessage }}
</BFormInvalidFeedback>
</BFormGroup>
</template>
<BInputGroup v-else append="GDD" :label="label" :label-for="labelFor">
<BFormInput
:id="labelFor"
v-focus="amountFocused"
:model-value="value"
:name="name"
:placeholder="placeholder"
type="text"
readonly
trim
/>
</BInputGroup>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useField } from 'vee-validate'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
const props = defineProps({
rules: {
type: Object,
default: () => ({}),
},
typ: { type: String, default: 'TransactionForm' },
name: { type: String, required: true, default: 'Amount' },
label: { type: String, required: true, default: 'Amount' },
placeholder: { type: String, required: true, default: 'Amount' },
balance: { type: Number, default: 0.0 },
disabled: { required: false, type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue'])
const route = useRoute()
const { n } = useI18n()
const { value, meta, errorMessage } = useField(props.name, props.rules)
const amountFocused = ref(false)
const amountValue = ref(0.0)
const labelFor = computed(() => props.name + '-input-field')
watch(value, (newValue) => {
emit('update:modelValue', newValue)
})
watch(
() => props.modelValue,
(newValue) => {
if (newValue !== value.value) value.value = newValue
},
)
const normalizeAmount = (inputValue) => {
amountFocused.value = false
if (typeof inputValue === 'string' && inputValue.length > 1) {
value.value = inputValue.replace(',', '.')
}
}
</script>

View File

@ -1,61 +0,0 @@
<template>
<BFormGroup :label="label" :label-for="labelFor" data-test="input-identifier">
<BFormInput
:id="labelFor"
:model-value="value"
:name="name"
:placeholder="placeholder"
type="text"
:state="meta.valid"
trim
class="bg-248"
:disabled="disabled"
autocomplete="off"
@update:model-value="value = $event"
/>
<BFormInvalidFeedback v-if="errorMessage">
{{ errorMessage }}
</BFormInvalidFeedback>
</BFormGroup>
</template>
<script setup>
import { computed, watch } from 'vue'
import { useField } from 'vee-validate'
const props = defineProps({
rules: {
type: Object,
default: () => ({
required: true,
validIdentifier: true,
}),
},
name: { type: String, required: true },
label: { type: String, required: true },
placeholder: { type: String, required: true },
modelValue: { type: String },
disabled: { type: Boolean, required: false, default: false },
})
const emit = defineEmits(['update:modelValue', 'onValidation'])
const { value, meta, errorMessage } = useField(props.name, props.rules, {
initialValue: props.modelValue,
})
const labelFor = computed(() => props.name + '-input-field')
watch(value, (newValue) => {
emit('update:modelValue', newValue)
})
watch(
() => props.modelValue,
(newValue) => {
if (newValue !== value.value) {
value.value = newValue
emit('onValidation')
}
},
)
</script>

View File

@ -1,125 +0,0 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import InputTextarea from './InputTextarea'
import { useField } from 'vee-validate'
import { BFormGroup, BFormInvalidFeedback, BFormTextarea } from 'bootstrap-vue-next'
vi.mock('vee-validate', () => ({
useField: vi.fn(),
}))
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key,
}),
}))
describe('InputTextarea', () => {
let wrapper
const createWrapper = (props = {}) => {
return mount(InputTextarea, {
props: {
rules: {},
name: 'input-field-name',
label: 'input-field-label',
placeholder: 'input-field-placeholder',
...props,
},
global: {
components: {
BFormGroup,
BFormTextarea,
BFormInvalidFeedback,
},
},
})
}
beforeEach(() => {
vi.mocked(useField).mockReturnValue({
value: '',
errorMessage: '',
meta: { valid: true },
})
wrapper = createWrapper()
})
it('renders the component InputTextarea', () => {
expect(wrapper.find('[data-test="input-textarea"]').exists()).toBe(true)
})
it('has a textarea field', () => {
expect(wrapper.findComponent({ name: 'BFormTextarea' }).exists()).toBe(true)
})
describe('properties', () => {
it('has the correct id', () => {
const textarea = wrapper.findComponent({ name: 'BFormTextarea' })
expect(textarea.attributes('id')).toBe('input-field-name-input-field')
})
it('has the correct placeholder', () => {
const textarea = wrapper.findComponent({ name: 'BFormTextarea' })
expect(textarea.attributes('placeholder')).toBe('input-field-placeholder')
})
it('has the correct label', () => {
const label = wrapper.find('label')
expect(label.text()).toBe('input-field-label')
})
it('has the correct label-for attribute', () => {
const label = wrapper.find('label')
expect(label.attributes('for')).toBe('input-field-name-input-field')
})
})
describe('input value changes', () => {
it('updates the model value when input changes', async () => {
const wrapper = mount(InputTextarea, {
props: {
rules: {},
name: 'input-field-name',
label: 'input-field-label',
placeholder: 'input-field-placeholder',
},
global: {
components: {
BFormGroup,
BFormInvalidFeedback,
BFormTextarea,
},
},
})
const textarea = wrapper.find('textarea')
await textarea.setValue('New Text')
expect(wrapper.vm.currentValue).toBe('New Text')
})
})
describe('disabled state', () => {
it('disables the textarea when disabled prop is true', async () => {
await wrapper.setProps({ disabled: true })
const textarea = wrapper.findComponent({ name: 'BFormTextarea' })
expect(textarea.attributes('disabled')).toBeDefined()
})
})
it('shows error message when there is an error', async () => {
vi.mocked(useField).mockReturnValue({
value: '',
errorMessage: 'This field is required',
meta: { valid: false },
})
wrapper = createWrapper()
await wrapper.vm.$nextTick()
const errorFeedback = wrapper.findComponent({ name: 'BFormInvalidFeedback' })
expect(errorFeedback.exists()).toBe(true)
expect(errorFeedback.text()).toBe('This field is required')
})
})

View File

@ -1,64 +0,0 @@
<template>
<div>
<BFormGroup :label="label" :label-for="labelFor" data-test="input-textarea">
<BFormTextarea
:id="labelFor"
:model-value="currentValue"
class="bg-248"
:name="name"
:placeholder="placeholder"
:state="meta.valid"
trim
:rows="4"
:max-rows="4"
:disabled="disabled"
no-resize
@update:modelValue="currentValue = $event"
/>
<BFormInvalidFeedback v-if="errorMessage">
{{ translatedErrorString }}
</BFormInvalidFeedback>
</BFormGroup>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useField } from 'vee-validate'
import { useI18n } from 'vue-i18n'
import { translateYupErrorString } from '@/validationSchemas'
const props = defineProps({
rules: {
type: Object,
default: () => ({}),
},
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
placeholder: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
})
const { value: currentValue, errorMessage, meta } = useField(props.name, props.rules)
const { t } = useI18n()
const translatedErrorString = computed(() => translateYupErrorString(errorMessage, t))
const labelFor = computed(() => `${props.name}-input-field`)
</script>
<style lang="scss" scoped>
:deep(.form-control) {
height: unset;
}
</style>

View File

@ -0,0 +1,92 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import ValidatedInput from '@/components/Inputs/ValidatedInput.vue'
import * as yup from 'yup'
import { BFormInvalidFeedback, BFormInput, BFormTextarea, BFormGroup } from 'bootstrap-vue-next'
import LabeledInput from '@/components/Inputs/LabeledInput.vue'
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key,
n: (n) => String(n),
}),
}))
describe('ValidatedInput', () => {
let wrapper
const createWrapper = (props = {}) =>
mount(ValidatedInput, {
props: {
label: 'Test Label',
modelValue: '',
name: 'testInput',
rules: yup.string().required().min(3).default(''),
...props,
},
global: {
mocks: {
$t: (key) => key,
$i18n: {
locale: 'en',
},
$n: (n) => String(n),
},
components: {
BFormInvalidFeedback,
BFormInput,
BFormTextarea,
BFormGroup,
LabeledInput,
},
},
})
beforeEach(() => {
wrapper = createWrapper()
})
it('renders the label and input', () => {
expect(wrapper.text()).toContain('Test Label')
const input = wrapper.find('input')
expect(input.exists()).toBe(true)
})
it('starts with neutral validation state', () => {
const input = wrapper.find('input')
expect(input.classes()).not.toContain('is-valid')
expect(input.classes()).not.toContain('is-invalid')
})
it('shows green border when value is valid before blur', async () => {
await wrapper.setProps({ modelValue: 'validInput' })
await wrapper.vm.$nextTick()
const input = wrapper.find('input')
expect(input.classes()).toContain('is-valid')
expect(input.classes()).not.toContain('is-invalid')
})
it('does not show red border before blur even if invalid', async () => {
await wrapper.setProps({ modelValue: 'a' })
const input = wrapper.find('input')
expect(input.classes()).not.toContain('is-invalid')
})
it('shows red border and error message after blur when input is invalid', async () => {
await wrapper.setProps({ modelValue: 'a' })
const input = wrapper.find('input')
await input.trigger('blur')
await wrapper.vm.$nextTick()
expect(input.classes()).toContain('is-invalid')
expect(wrapper.text()).toContain('this must be at least 3 characters')
})
it('emits update:modelValue on input', async () => {
const input = wrapper.find('input')
await input.setValue('hello')
await wrapper.vm.$nextTick()
expect(wrapper.emitted()['update:modelValue']).toBeTruthy()
const [value, name] = wrapper.emitted()['update:modelValue'][0]
expect(value).toBe('hello')
expect(name).toBe('testInput')
})
})

View File

@ -9,7 +9,8 @@
:required="!isOptional"
:label="label"
:name="name"
:state="valid"
:state="smartValidState"
@blur="afterFirstInput = true"
@update:modelValue="updateValue"
>
<BFormInvalidFeedback v-if="errorMessage">
@ -19,7 +20,7 @@
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { computed, ref, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import LabeledInput from './LabeledInput'
import { translateYupErrorString } from '@/validationSchemas'
@ -38,19 +39,40 @@ const props = defineProps({
type: Object,
required: true,
},
disableSmartValidState: {
type: Boolean,
default: false,
},
})
const { t } = useI18n()
const model = ref(props.modelValue)
const model = ref(props.modelValue !== 0 ? props.modelValue : '')
// change to true after user leave the input field the first time
// prevent showing errors on form init
const afterFirstInput = ref(false)
const valid = computed(() => props.rules.isValidSync(props.modelValue))
const errorMessage = computed(() => {
if (props.modelValue === undefined || props.modelValue === '' || props.modelValue === null) {
return undefined
const valid = computed(() => props.rules.isValidSync(model.value))
// smartValidState controls the visual validation feedback for the input field.
// The goal is to avoid showing red (invalid) borders too early, creating a smoother UX:
//
// - On initial form open, the field is neutral (no validation state shown).
// - If the user enters a value that passes validation, we show a green (valid) state immediately.
// - We only show red (invalid) feedback *after* the user has blurred the field for the first time.
//
// Before first blur:
// - show green if valid, otherwise neutral (null)
// After first blur:
// - show true or false according to the validation result
const smartValidState = computed(() => {
if (afterFirstInput.value || props.disableSmartValidState) {
return valid.value
}
return valid.value ? true : null
})
const errorMessage = computed(() => {
try {
props.rules.validateSync(props.modelValue)
props.rules.validateSync(model.value)
return undefined
} catch (e) {
return translateYupErrorString(e.message, t)
@ -79,4 +101,17 @@ const minValue = computed(() => getTestParameter('min'))
const maxValue = computed(() => getTestParameter('max'))
const resetValue = computed(() => schemaDescription.value.default)
const isOptional = computed(() => schemaDescription.value.optional)
// reset on mount
onMounted(() => {
afterFirstInput.value = false
})
</script>
<!-- disable animation on invalid input -->
<style>
.form-control {
transition: none !important;
transform: none !important;
animation: none !important;
}
</style>

View File

@ -203,22 +203,43 @@
"username": "Benutzername",
"username-placeholder": "Wähle deinen Benutzernamen",
"validation": {
"gddCreationTime": {
"min": "Die Stunden sollten mindestens {min} groß sein",
"max": "Die Stunden sollten höchstens {max} groß sein",
"decimal-places": "Die Stunden sollten maximal zwei Nachkommastellen enthalten"
"amount": {
"min": "Der Betrag sollte mindestens {min} groß sein.",
"max": "Der Betrag sollte höchstens {max} groß sein.",
"decimal-places": "Der Betrag sollte maximal zwei Nachkommastellen enthalten.",
"typeError": "Der Betrag sollte eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein."
},
"gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein",
"is-not": "Du kannst dir selbst keine Gradidos überweisen",
"contributionDate": {
"required": "Das Beitragsdatum ist ein Pflichtfeld.",
"min": "Das Frühste Beitragsdatum ist {min}.",
"max": "Das Späteste Beitragsdatum ist heute, der {max}."
},
"contributionMemo": {
"min": "Die Tätigkeitsbeschreibung sollte mindestens {min} Zeichen lang sein.",
"max": "Die Tätigkeitsbeschreibung sollte höchstens {max} Zeichen lang sein.",
"required": "Die Tätigkeitsbeschreibung ist ein Pflichtfeld."
},
"hours": {
"min": "Die Stunden sollten mindestens {min} groß sein.",
"max": "Die Stunden sollten höchstens {max} groß sein.",
"decimal-places": "Die Stunden sollten maximal zwei Nachkommastellen enthalten.",
"typeError": "Die Stunden sollten eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein."
},
"gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein.",
"identifier": {
"required": "Der Empfänger ist ein Pflichtfeld.",
"typeError": "Der Empfänger muss eine Email, ein Nutzernamen oder eine Gradido ID sein."
},
"is-not": "Du kannst dir selbst keine Gradidos überweisen!",
"memo": {
"min": "Die Tätigkeitsbeschreibung sollte mindestens {min} Zeichen lang sein",
"max": "Die Tätigkeitsbeschreibung sollte höchstens {max} Zeichen lang sein"
"min": "Die Nachricht sollte mindestens {min} Zeichen lang sein.",
"max": "Die Nachricht sollte höchstens {max} Zeichen lang sein.",
"required": "Die Nachricht ist ein Pflichtfeld."
},
"requiredField": "{fieldName} ist ein Pflichtfeld",
"username-allowed-chars": "Der Nutzername darf nur aus Buchstaben (ohne Umlaute), Zahlen, Binde- oder Unterstrichen bestehen.",
"username-hyphens": "Binde- oder Unterstriche müssen zwischen Buchstaben oder Zahlen stehen.",
"username-unique": "Der Nutzername ist bereits vergeben.",
"valid-identifier": "Muss eine Email, ein Nutzernamen oder eine gradido ID sein."
"username-unique": "Der Nutzername ist bereits vergeben."
},
"your_amount": "Dein Betrag"
},

View File

@ -203,22 +203,43 @@
"username": "Username",
"username-placeholder": "Choose your username",
"validation": {
"gddCreationTime": {
"min": "The hours should be at least {min} in size",
"max": "The hours should not be larger than {max}",
"decimal-places": "The hours should contain a maximum of two decimal places"
"amount": {
"min": "The amount should be at least {min} in size.",
"max": "The amount should not be larger than {max}.",
"decimal-places": "The amount should contain a maximum of two decimal places.",
"typeError": "The amount should be a number between {min} and {max} with at most two digits after the decimal point."
},
"gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point",
"is-not": "You cannot send Gradidos to yourself",
"contributionDate": {
"required": "The contribution date is a required field.",
"min": "The earliest contribution date is {min}.",
"max": "The latest contribution date is today, {max}."
},
"contributionMemo": {
"min": "The job description should be at least {min} characters long.",
"max": "The job description should not be longer than {max} characters.",
"required": "The job description is required."
},
"hours": {
"min": "The hours should be at least {min} in size.",
"max": "The hours should not be larger than {max}.",
"decimal-places": "The hours should contain a maximum of two decimal places.",
"typeError": "The hours should be a number between {min} and {max} with at most two digits after the decimal point."
},
"gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point.",
"identifier": {
"required": "The recipient is a required field.",
"typeError": "The recipient must be an email, a username or a Gradido ID."
},
"is-not": "You cannot send Gradidos to yourself!",
"memo": {
"min": "The job description should be at least {min} characters long",
"max": "The job description should not be longer than {max} characters"
"min": "The message should be at least {min} characters long.",
"max": "The message should not be longer than {max} characters.",
"required": "The message is required."
},
"requiredField": "The {fieldName} field is required",
"username-allowed-chars": "The username may only contain letters, numbers, hyphens or underscores.",
"username-hyphens": "Hyphens or underscores must be in between letters or numbers.",
"username-unique": "This username is already taken.",
"valid-identifier": "Must be a valid email, username or gradido ID."
"username-unique": "This username is already taken."
},
"your_amount": "Your amount"
},

View File

@ -11,9 +11,7 @@ import nl from '@vee-validate/i18n/dist/locale/nl.json'
import tr from '@vee-validate/i18n/dist/locale/tr.json'
import { useI18n } from 'vue-i18n'
// Email and username regex patterns remain the same
const EMAIL_REGEX =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
// username regex pattern remain the same
const USERNAME_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/
export const loadAllRules = (i18nCallback, apollo) => {
@ -48,22 +46,6 @@ export const loadAllRules = (i18nCallback, apollo) => {
defineRule('max', max)
// ------ Custom rules ------
defineRule('gddSendAmount', (value, { min, max }) => {
value = value.replace(',', '.')
return value.match(/^[0-9]+(\.[0-9]{0,2})?$/) && Number(value) >= min && Number(value) <= max
? true
: i18nCallback.t('form.validation.gddSendAmount', {
min: i18nCallback.n(min, 'ungroupedDecimal'),
max: i18nCallback.n(max, 'ungroupedDecimal'),
})
})
defineRule('gddCreationTime', (value, { min, max }) => {
return value >= min && value <= max
? true
: i18nCallback.t('form.validation.gddCreationTime', { min, max })
})
defineRule('is_not', (value, [otherValue]) => {
return value !== otherValue
? true
@ -122,13 +104,4 @@ export const loadAllRules = (i18nCallback, apollo) => {
})
return data.checkUsername || i18nCallback.t('form.validation.username-unique')
})
defineRule('validIdentifier', (value) => {
const isEmail = !!EMAIL_REGEX.test(value)
const isUsername = !!value.match(USERNAME_REGEX)
const isGradidoId = validateUuid(value) && versionUuid(value) === 4
return (
isEmail || isUsername || isGradidoId || i18nCallback.t('form.validation.valid-identifier')
)
})
}

View File

@ -1,4 +1,10 @@
import { string } from 'yup'
import { validate as validateUuid, version as versionUuid } from 'uuid'
// Email and username regex patterns remain the same
const EMAIL_REGEX =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const USERNAME_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/
// TODO: only needed for grace period, before all inputs updated for using veeValidate + yup
export const isLanguageKey = (str) =>
@ -16,6 +22,16 @@ export const translateYupErrorString = (error, t) => {
}
export const memo = string()
.required('contribution.yourActivity')
.required('form.validation.memo.required')
.min(5, ({ min }) => ({ key: 'form.validation.memo.min', values: { min } }))
.max(255, ({ max }) => ({ key: 'form.validation.memo.max', values: { max } }))
export const identifier = string()
.required('form.validation.identifier.required')
.test('valid-identifier', 'form.validation.identifier.typeError', (value) => {
const isEmail = !!EMAIL_REGEX.test(value)
const isUsername = !!value.match(USERNAME_REGEX)
// TODO: use valibot and rules from shared
const isGradidoId = validateUuid(value) && versionUuid(value) === 4
return isEmail || isUsername || isGradidoId
})

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "gradido",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido",
"main": "index.js",
"repository": "git@github.com:gradido/gradido.git",

View File

@ -1,6 +1,6 @@
{
"name": "shared",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido Shared Code, Low-Level Shared Code, without dependencies on other modules",
"main": "./build/index.js",
"types": "./src/index.ts",

2439
yarn.lock

File diff suppressed because it is too large Load Diff