Compare commits

...

22 Commits

Author SHA1 Message Date
Matthew Raymer
a92d088432 fix: resolve Playwright strict mode violations in test suite
- Fix multiple button selector conflicts in registration and export dialogs
- Add specific selectors for 'Yes' and 'No' buttons in registration dialog
- Add comprehensive modal overlay handling for export dialog dismissal
- Fix close button selectors to target specific alert content
- Resolve test failures in 'Check User 0 can register a random person' and 'New offers for another user'

All previously failing tests now pass successfully.
2025-08-27 03:48:58 +00:00
aa346a9abd feat: for confirm list, allow to see 30 minutes before meeting start 2025-08-26 20:29:26 -06:00
9ea2f96106 fix: Fix onboard-meeting-members deep link with groupId.
This was cherry-picked from master-patch
2025-08-26 20:28:31 -06:00
623bf12ecd doc: add simpler instruction to get started with testing 2025-08-26 19:25:31 -06:00
Matthew Raymer
427660d686 docs: enhance serve command documentation in BUILDING.md
- Clarify build:web:serve purpose as "production testing"
- Add "Why Use serve?" section explaining benefits
- Document SPA routing support for deep links (/discover, /account)
- Add dedicated "Local Serving with serve" technical section
- Explain server options (npx serve vs Python fallback)
- Improve developer understanding of when and why to use serve

Fixes documentation gap identified in serve command usage
2025-08-26 09:58:35 +00:00
Matthew Raymer
643f31c43a Merge branch 'master' into build-web-serve-test 2025-08-26 09:52:49 +00:00
2c6b787fa2 Merge pull request 'Show current user in ContactGiftingView' (#155) from contact-gifting-current-user into master
Reviewed-on: #155
2025-08-26 05:00:10 -04:00
Jose Olarte III
ec53452220 Chore: lint fix 2025-08-26 16:52:32 +08:00
Jose Olarte III
ec326495b2 Merge branch 'master' into contact-gifting-current-user 2025-08-26 16:51:09 +08:00
Jose Olarte III
cc50c38d13 Chore: simplify wording for person entities
- Replaced all instances of "Unnamed Member" with "Someone Unnamed"
- Removed unused UNNAMED_MEMBER constant
- Renamed UNNAMED_PERSON to THAT_UNNAMED_PERSON to match its value
- Renamed UNNAMED_USER to UNNAMED_PERSON and changed the value to "unnamed person"
2025-08-26 16:32:57 +08:00
3969167d92 Merge pull request 'feat: implement safe area insets for Android and add development tooling' (#182) from android-safe-area-insets into master
Reviewed-on: #182
2025-08-26 03:29:50 -04:00
Matthew Raymer
e3cc22245c fix(build): resolve web build script export error and add missing serve dependency
- Fix malformed multi-line comment in .env.test causing shell parsing failure
- Add serve@14.2.4 as dev dependency to eliminate build-time installation prompts
- Resolves "export: ' production).=': not a valid identifier" error
- Test environment builds now complete successfully without user interaction

Fixes test environment build blocking issue
2025-08-19 11:38:32 +00:00
Jose Olarte III
eb44e7b51e Chore: convert "unnamed" into constant
- "Unnamed/Unknown" simplified into just "Unnamed"
- Phrase variations have their own constants
2025-08-18 20:33:19 +08:00
Jose Olarte III
ca8d72e1c9 Fix: remove non-applicable IDs
- Projects use handleID, not DID
2025-08-18 16:43:15 +08:00
Jose Olarte III
a4528c5703 Refactor: eliminate "special" entity type and use DID-based logic
Replace string-based entity type matching with DID-based logic for "You" and "Unnamed" entities. Treat these as regular person entities instead of special types.

- Remove "special" type from EntitySelectionEvent interface
- Update EntityGrid to emit "You" and "Unnamed" as person entities
- Simplify SpecialEntityCard emit structure (remove entityType parameter)
- Refactor GiftedDialog to process all person entities with DID-based logic
- Update ContactGiftingView and HomeView to use DID-based entity creation
- Remove string literals "You" and "Unnamed" from method signatures
2025-08-15 13:22:49 +08:00
Jose Olarte III
6acebb66ef Merge branch 'master' into contact-gifting-current-user 2025-08-15 10:28:13 +08:00
b3f7026afe Merge branch 'master' into contact-gifting-current-user 2025-08-12 06:35:08 -04:00
Jose Olarte III
ec1a725832 Refactor: improve dialog logic and entity handling
- Split openDialog into separate methods to improve code readability and maintainability through method extraction
- Add receiver name fallback in GiftedDialog when receiver exists but has no name
- Enhance shouldShowYouEntity to prevent selecting "You" as both giver and recipient
- Improve labeling of "(No name)" entities while retaining original entity object properties
- Apply special styling to "Unnamed" and "(No Name)" entities
2025-08-12 18:34:47 +08:00
Jose Olarte III
6d316c2b3f Fix: restored prop to show/hide "Show All" 2025-08-11 19:14:35 +08:00
Jose Olarte III
24f6730572 Fix: remove redundant method 2025-08-11 19:06:42 +08:00
Jose Olarte III
0fc44b31bf Merge branch 'master' into contact-gifting-current-user 2025-08-11 19:01:06 +08:00
Jose Olarte III
bed2c7106a Added: current user in ContactGiftingView
- "You" is displayed conditionally, similar to GiftedDialog behavior
- "Show All" is hidden in GiftedDialog when accessed from ContactGiftingView (redundant)
2025-07-28 18:46:56 +08:00
31 changed files with 1094 additions and 301 deletions

View File

@@ -251,7 +251,7 @@ npm run build:web:dev # Start development server with hot reload
npm run build:web # Development build (starts dev server with hot reload)
npm run build:web:test # Test environment build (optimized for testing)
npm run build:web:prod # Production build (optimized for production)
npm run build:web:serve # Build and serve locally (builds then serves)
npm run build:web:serve # Build and serve locally for production testing
# Docker builds
npm run build:web:docker # Development build with Docker containerization
@@ -269,6 +269,12 @@ Start the development server using `npm run build:web:dev` or `npm run build:web
2. The built files will be in the `dist` directory
3. To test the production build locally, use `npm run build:web:serve` (builds then serves)
**Why Use `serve`?**
- **Production Testing**: Test your optimized production build locally before deployment
- **SPA Routing Validation**: Verify deep linking and navigation work correctly (handles routes like `/discover`, `/account`)
- **Performance Testing**: Test the minified and optimized build locally
- **Deployment Validation**: Ensure built files work correctly when served by a real HTTP server
You'll likely want to use test locations for the Endorser & image & partner servers; see "DEFAULT_ENDORSER_API_SERVER" & "DEFAULT_IMAGE_API_SERVER" & "DEFAULT_PARTNER_API_SERVER" below.
### Web Build Script Details
@@ -288,7 +294,7 @@ All web build commands use the `./scripts/build-web.sh` script, which provides:
- **Clean Build**: Removes previous `dist/` directory
- **Vite Build**: Executes `npx vite build --config vite.config.web.mts`
- **Docker Support**: Optional Docker containerization
- **Local Serving**: Built-in HTTP server for testing builds
- **Local Serving**: Built-in HTTP server for testing builds with SPA routing support
**Direct Script Usage:**
@@ -324,6 +330,25 @@ All web build commands use the `./scripts/build-web.sh` script, which provides:
- `5` - Serve command failed
- `6` - Invalid build mode
### Local Serving with `serve`
The `serve` functionality provides a local HTTP server for testing production builds:
**What It Does:**
1. **Builds** the application using Vite
2. **Serves** the built files from the `dist/` directory
3. **Handles SPA Routing** - serves `index.html` for all routes (fixes 404s on `/discover`, `/account`, etc.)
**Server Options:**
- **Primary**: `npx serve -s dist -l 8080` (recommended - full SPA support)
- **Fallback**: Python HTTP server (limited SPA routing support)
**Use Cases:**
- Testing production builds before deployment
- Validating SPA routing behavior
- Performance testing of optimized builds
- Debugging production build issues locally
### Compile and minify for test & production
- If there are DB changes: before updating the test server, open browser(s) with

View File

@@ -18,7 +18,7 @@ npm install
npm run build:web:serve -- --test
```
To be able to make submissions: go to "profile" (bottom left), go to the bottom and expand "Show Advanced Settings", go to the bottom and to the "Test Page", and finally "Become User 0" to see all the functionality.
To be able to take action on the platform: go to [the test page](http://localhost:8080/test) and click "Become User 0".
See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker).

596
package-lock.json generated
View File

@@ -138,6 +138,7 @@
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"rimraf": "^6.0.1",
"serve": "^14.2.4",
"tailwindcss": "^3.4.1",
"ts-jest": "^29.4.0",
"tsx": "^4.20.4",
@@ -11760,6 +11761,13 @@
"node": ">=10.0.0"
}
},
"node_modules/@zeit/schemas": {
"version": "2.36.0",
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz",
"integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==",
"dev": true,
"license": "MIT"
},
"node_modules/@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
@@ -11822,9 +11830,8 @@
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
@@ -11949,6 +11956,16 @@
"optional": true,
"peer": true
},
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
"integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.1.0"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -12153,6 +12170,27 @@
"license": "ISC",
"optional": true
},
"node_modules/arch": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz",
"integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/are-we-there-yet": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz",
@@ -12949,6 +12987,153 @@
"dev": true,
"license": "ISC"
},
"node_modules/boxen": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz",
"integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-align": "^3.0.1",
"camelcase": "^7.0.0",
"chalk": "^5.0.1",
"cli-boxes": "^3.0.0",
"string-width": "^5.1.2",
"type-fest": "^2.13.0",
"widest-line": "^4.0.1",
"wrap-ansi": "^8.0.1"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boxen/node_modules/ansi-regex": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/boxen/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/boxen/node_modules/camelcase": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz",
"integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boxen/node_modules/chalk": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz",
"integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/boxen/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,
"license": "MIT"
},
"node_modules/boxen/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,
"license": "MIT",
"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/boxen/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/boxen/node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/boxen/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,
"license": "MIT",
"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/bplist-creator": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@@ -13735,6 +13920,22 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk-template": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
"integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/chalk-template?sponsor=1"
}
},
"node_modules/char-regex": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
@@ -13959,6 +14160,19 @@
"node": ">=6"
}
},
"node_modules/cli-boxes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
"integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -14015,6 +14229,24 @@
"node": ">=8"
}
},
"node_modules/clipboardy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz",
"integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==",
"dev": true,
"license": "MIT",
"dependencies": {
"arch": "^2.2.0",
"execa": "^5.1.1",
"is-wsl": "^2.2.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -14181,9 +14413,8 @@
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"mime-db": ">= 1.43.0 < 2"
},
@@ -14457,6 +14688,16 @@
"license": "ISC",
"optional": true
},
"node_modules/content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/conventional-changelog": {
"version": "3.1.25",
"resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.25.tgz",
@@ -19249,6 +19490,19 @@
"node": ">=0.10.0"
}
},
"node_modules/is-port-reachable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz",
"integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -24440,9 +24694,8 @@
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -25906,6 +26159,13 @@
"node": ">=0.10.0"
}
},
"node_modules/path-is-inside": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
"integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
"dev": true,
"license": "(WTFPL OR MIT)"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -25947,6 +26207,13 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/path-to-regexp": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
"integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
"dev": true,
"license": "MIT"
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -28112,6 +28379,30 @@
"integrity": "sha512-CiD3ZSanZqcMPRhtfct5K9f7i3OLCcBBWsJjLh1gW9RO/nS94sVzY59iS+fgYBOBqaBpf4EzfqUF3j9IG+xo8A==",
"license": "MIT"
},
"node_modules/registry-auth-token": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz",
"integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"rc": "^1.1.6",
"safe-buffer": "^5.0.1"
}
},
"node_modules/registry-url": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz",
"integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"rc": "^1.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/regjsgen": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
@@ -29220,6 +29511,115 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/serve": {
"version": "14.2.4",
"resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz",
"integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@zeit/schemas": "2.36.0",
"ajv": "8.12.0",
"arg": "5.0.2",
"boxen": "7.0.0",
"chalk": "5.0.1",
"chalk-template": "0.4.0",
"clipboardy": "3.0.0",
"compression": "1.7.4",
"is-port-reachable": "4.0.0",
"serve-handler": "6.1.6",
"update-check": "1.5.4"
},
"bin": {
"serve": "build/main.js"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/serve-handler": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
"integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "3.0.0",
"content-disposition": "0.5.2",
"mime-types": "2.1.18",
"minimatch": "3.1.2",
"path-is-inside": "1.0.2",
"path-to-regexp": "3.3.0",
"range-parser": "1.2.0"
}
},
"node_modules/serve-handler/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/serve-handler/node_modules/bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve-handler/node_modules/mime-db": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/serve-handler/node_modules/mime-types": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "~1.33.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/serve-handler/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/serve-handler/node_modules/range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
"integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
@@ -29351,6 +29751,106 @@
"node": ">= 0.8"
}
},
"node_modules/serve/node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/serve/node_modules/bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve/node_modules/chalk": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz",
"integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/serve/node_modules/compression": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
"integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"accepts": "~1.3.5",
"bytes": "3.0.0",
"compressible": "~2.0.16",
"debug": "2.6.9",
"on-headers": "~1.0.2",
"safe-buffer": "5.1.2",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/serve/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/serve/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/serve/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true,
"license": "MIT"
},
"node_modules/serve/node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/serve/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT"
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -31680,6 +32180,17 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/update-check": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz",
"integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"registry-auth-token": "3.3.2",
"registry-url": "3.1.0"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -31788,9 +32299,8 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 0.8"
}
@@ -32642,6 +33152,76 @@
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/widest-line": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
"integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==",
"dev": true,
"license": "MIT",
"dependencies": {
"string-width": "^5.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/widest-line/node_modules/ansi-regex": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
"integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/widest-line/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,
"license": "MIT"
},
"node_modules/widest-line/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,
"license": "MIT",
"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/widest-line/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/wonka": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz",

View File

@@ -268,6 +268,7 @@
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"rimraf": "^6.0.1",
"serve": "^14.2.4",
"tailwindcss": "^3.4.1",
"ts-jest": "^29.4.0",
"tsx": "^4.20.4",

View File

@@ -22,7 +22,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */
<!-- "Unnamed" entity -->
<SpecialEntityCard
entity-type="unnamed"
label="Unnamed"
:label="unnamedEntityName"
icon="circle-question"
:entity-data="unnamedEntityData"
:notify="notify"
@@ -83,6 +83,7 @@ import ShowAllCard from "./ShowAllCard.vue";
import { Contact } from "../db/tables/contacts";
import { PlanData } from "../interfaces/records";
import { NotificationIface } from "../constants/app";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
/**
* EntityGrid - Unified grid layout for displaying people or projects
@@ -159,6 +160,10 @@ export default class EntityGrid extends Vue {
@Prop({ default: "other party" })
conflictContext!: string;
/** Whether to hide the "Show All" navigation */
@Prop({ default: false })
hideShowAll!: boolean;
/**
* Function to determine which entities to display (allows parent control)
*
@@ -245,7 +250,9 @@ export default class EntityGrid extends Vue {
* Whether to show the "Show All" navigation
*/
get shouldShowAll(): boolean {
return this.entities.length > 0 && this.showAllRoute !== "";
return (
!this.hideShowAll && this.entities.length > 0 && this.showAllRoute !== ""
);
}
/**
@@ -271,10 +278,17 @@ export default class EntityGrid extends Vue {
get unnamedEntityData(): { did: string; name: string } {
return {
did: "",
name: "Unnamed",
name: UNNAMED_ENTITY_NAME,
};
}
/**
* Get the unnamed entity name constant
*/
get unnamedEntityName(): string {
return UNNAMED_ENTITY_NAME;
}
/**
* Check if a person DID is conflicted
*/
@@ -304,16 +318,13 @@ export default class EntityGrid extends Vue {
/**
* Handle special entity selection from SpecialEntityCard
* Treat "You" and "Unnamed" as person entities
*/
handleEntitySelected(event: {
type: string;
entityType: string;
data: { did?: string; name: string };
}): void {
handleEntitySelected(event: { data: { did?: string; name: string } }): void {
// Convert special entities to person entities since they represent people
this.emitEntitySelected({
type: "special",
entityType: event.entityType,
data: event.data,
type: "person",
data: event.data as Contact,
});
}
@@ -321,13 +332,11 @@ export default class EntityGrid extends Vue {
@Emit("entity-selected")
emitEntitySelected(data: {
type: "person" | "project" | "special";
entityType?: string;
data: Contact | PlanData | { did?: string; name: string };
type: "person" | "project";
data: Contact | PlanData;
}): {
type: "person" | "project" | "special";
entityType?: string;
data: Contact | PlanData | { did?: string; name: string };
type: "person" | "project";
data: Contact | PlanData;
} {
return data;
}

View File

@@ -27,6 +27,7 @@ Matthew Raymer */
:show-all-query-params="showAllQueryParams"
:notify="notify"
:conflict-context="conflictContext"
:hide-show-all="hideShowAll"
@entity-selected="handleEntitySelected"
/>
@@ -55,9 +56,8 @@ interface EntityData {
* Entity selection event data structure
*/
interface EntitySelectionEvent {
type: "person" | "project" | "special";
entityType?: string;
data: Contact | PlanData | EntityData;
type: "person" | "project";
data: Contact | PlanData;
}
/**
@@ -154,6 +154,10 @@ export default class EntitySelectionStep extends Vue {
@Prop()
notify?: (notification: NotificationIface, timeout?: number) => void;
/** Whether to hide the "Show All" navigation */
@Prop({ default: false })
hideShowAll!: boolean;
/**
* CSS classes for the cancel button
*/

View File

@@ -42,8 +42,8 @@ computed CSS properties * * @author Matthew Raymer */
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
{{ label }}
</p>
<h3 class="font-semibold truncate">
{{ entity?.name || "Unnamed" }}
<h3 :class="nameClasses">
{{ displayName }}
</h3>
</div>
@@ -62,6 +62,7 @@ import { Component, Prop, Vue } from "vue-facing-decorator";
import EntityIcon from "./EntityIcon.vue";
import ProjectIcon from "./ProjectIcon.vue";
import { Contact } from "../db/tables/contacts";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
/**
* Entity interface for both person and project entities
@@ -138,6 +139,38 @@ export default class EntitySummaryButton extends Vue {
return this.editable ? "text-blue-500" : "text-slate-400";
}
/**
* Computed CSS classes for the entity name
*/
get nameClasses(): string {
const baseClasses = "font-semibold truncate";
// Add italic styling for special "Unnamed" or entities without set names
if (!this.entity?.name || this.entity?.did === "") {
return `${baseClasses} italic text-slate-500`;
}
return baseClasses;
}
/**
* Computed display name for the entity
*/
get displayName(): string {
// If the entity has a set name, use that name
if (this.entity?.name) {
return this.entity.name;
}
// If the entity is the special "Unnamed", use "Unnamed"
if (this.entity?.did === "") {
return UNNAMED_ENTITY_NAME;
}
// If the entity does not have a set name, but is not the special "Unnamed", use their DID
return this.entity?.did;
}
/**
* Handle click event - only call function prop if editable
* Allows parent to control edit behavior and validation

View File

@@ -29,6 +29,7 @@
:unit-code="unitCode"
:offer-id="offerId"
:notify="$notify"
:hide-show-all="hideShowAll"
@entity-selected="handleEntitySelected"
@cancel="cancel"
/>
@@ -87,6 +88,7 @@ import {
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER,
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE,
} from "@/constants/notifications";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
@Component({
components: {
@@ -114,6 +116,7 @@ export default class GiftedDialog extends Vue {
@Prop() fromProjectId = "";
@Prop() toProjectId = "";
@Prop() isFromProjectView = false;
@Prop() hideShowAll = false;
@Prop({ default: "person" }) giverEntityType = "person" as
| "person"
| "project";
@@ -224,15 +227,6 @@ export default class GiftedDialog extends Vue {
this.allMyDids = await retrieveAccountDids();
if (this.giver && !this.giver.name) {
this.giver.name = didInfo(
this.giver.did,
this.activeDid,
this.allMyDids,
this.allContacts,
);
}
if (
this.giverEntityType === "project" ||
this.recipientEntityType === "project"
@@ -455,14 +449,14 @@ export default class GiftedDialog extends Vue {
if (contact) {
this.giver = {
did: contact.did,
name: contact.name || contact.did,
name: contact.name,
};
} else {
// Only set to "Unnamed" if no giver is currently set
if (!this.giver || !this.giver.did) {
this.giver = {
did: "",
name: "Unnamed",
name: UNNAMED_ENTITY_NAME,
};
}
}
@@ -517,14 +511,14 @@ export default class GiftedDialog extends Vue {
if (contact) {
this.receiver = {
did: contact.did,
name: contact.name || contact.did,
name: contact.name,
};
} else {
// Only set to "Unnamed" if no receiver is currently set
if (!this.receiver || !this.receiver.did) {
this.receiver = {
did: "",
name: "Unnamed",
name: UNNAMED_ENTITY_NAME,
};
}
}
@@ -566,20 +560,21 @@ export default class GiftedDialog extends Vue {
/**
* Handle entity selection from EntitySelectionStep
* @param entity - The selected entity (person, project, or special) with stepType
* @param entity - The selected entity (person or project) with stepType
*/
handleEntitySelected(entity: {
type: "person" | "project" | "special";
entityType?: string;
data: Contact | PlanData | { did?: string; name: string };
type: "person" | "project";
data: Contact | PlanData;
stepType: string;
}) {
if (entity.type === "person") {
const contact = entity.data as Contact;
// Apply DID-based logic for person entities
const processedContact = this.processPersonEntity(contact);
if (entity.stepType === "giver") {
this.selectGiver(contact);
this.selectGiver(processedContact);
} else {
this.selectRecipient(contact);
this.selectRecipient(processedContact);
}
} else if (entity.type === "project") {
const project = entity.data as PlanData;
@@ -588,33 +583,22 @@ export default class GiftedDialog extends Vue {
} else {
this.selectRecipientProject(project);
}
} else if (entity.type === "special") {
// Handle special entities like "You" and "Unnamed"
if (entity.entityType === "you") {
// "You" entity selected
const youEntity = {
did: this.activeDid,
name: "You",
};
if (entity.stepType === "giver") {
this.giver = youEntity;
} else {
this.receiver = youEntity;
}
this.firstStep = false;
} else if (entity.entityType === "unnamed") {
// "Unnamed" entity selected
const unnamedEntity = {
did: "",
name: "Unnamed",
};
if (entity.stepType === "giver") {
this.giver = unnamedEntity;
} else {
this.receiver = unnamedEntity;
}
this.firstStep = false;
}
}
}
/**
* Processes person entities using DID-based logic for "You" and "Unnamed"
*/
private processPersonEntity(contact: Contact): Contact {
if (contact.did === this.activeDid) {
// If DID matches active DID, create "You" entity
return { ...contact, name: "You" };
} else if (!contact.did || contact.did === "") {
// If DID is empty/null, create "Unnamed" entity
return { ...contact, name: UNNAMED_ENTITY_NAME };
} else {
// Return the contact as-is
return contact;
}
}

View File

@@ -81,7 +81,7 @@
<div class="flex items-center justify-between">
<div class="flex items-center">
<h3 class="text-lg font-medium">
{{ member.name || "Unnamed Member" }}
{{ member.name || unnamedMember }}
</h3>
<div
v-if="!getContactFor(member.did) && member.did !== activeDid"
@@ -177,6 +177,7 @@ import {
NOTIFY_ADD_CONTACT_FIRST,
NOTIFY_CONTINUE_WITHOUT_ADDING,
} from "@/constants/notifications";
import { SOMEONE_UNNAMED } from "@/constants/entities";
interface Member {
admitted: boolean;
@@ -220,6 +221,13 @@ export default class MembersList extends Vue {
apiServer = "";
contacts: Array<Contact> = [];
/**
* Get the unnamed member constant
*/
get unnamedMember(): string {
return SOMEONE_UNNAMED;
}
async created() {
this.notify = createNotifyHelpers(this.$notify);

View File

@@ -25,7 +25,7 @@ conflict detection. * * @author Matthew Raymer */
</div>
<h3 :class="nameClasses">
{{ person.name || person.did || "Unnamed" }}
{{ displayName }}
</h3>
</li>
</template>
@@ -98,9 +98,27 @@ export default class PersonCard extends Vue {
return `${baseClasses} text-slate-400`;
}
// Add italic styling for entities without set names
if (!this.person.name) {
return `${baseClasses} italic text-slate-500`;
}
return baseClasses;
}
/**
* Computed display name for the person
*/
get displayName(): string {
// If the entity has a set name, use that name
if (this.person.name) {
return this.person.name;
}
// If the entity does not have a set name
return this.person.did;
}
/**
* Handle card click - emit if selectable and not conflicted, show warning if conflicted
*/
@@ -114,7 +132,7 @@ export default class PersonCard extends Vue {
group: "alert",
type: "warning",
title: "Cannot Select",
text: `You cannot select "${this.person.name || this.person.did || "Unnamed"}" because they are already selected as the ${this.conflictContext}.`,
text: `You cannot select "${this.displayName}" because they are already selected as the ${this.conflictContext}.`,
},
3000,
);

View File

@@ -15,7 +15,7 @@ issuer information. * * @author Matthew Raymer */
<h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
{{ project.name || "Unnamed Project" }}
{{ project.name || unnamedProject }}
</h3>
<div class="text-xs text-slate-500 truncate">
@@ -31,6 +31,7 @@ import ProjectIcon from "./ProjectIcon.vue";
import { PlanData } from "../interfaces/records";
import { Contact } from "../db/tables/contacts";
import { didInfo } from "../libs/endorserServer";
import { UNNAMED_PROJECT } from "@/constants/entities";
/**
* ProjectCard - Displays a project entity with selection capability
@@ -63,6 +64,13 @@ export default class ProjectCard extends Vue {
@Prop({ required: true })
allContacts!: Contact[];
/**
* Get the unnamed project constant
*/
get unnamedProject(): string {
return UNNAMED_PROJECT;
}
/**
* Computed display name for the project issuer
*/

View File

@@ -115,6 +115,7 @@ import { urlBase64ToUint8Array } from "../libs/crypto/vc/util";
import * as libsUtil from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
// Example interface for error
interface ErrorResponse {
@@ -602,7 +603,7 @@ export default class PushNotificationPermission extends Vue {
* Returns the default message for direct push
*/
get notificationMessagePlaceholder(): string {
return "Click to share some gratitude with the world -- even if they're unnamed.";
return `Click to share some gratitude with the world -- even if they're ${UNNAMED_ENTITY_NAME.toLowerCase()}.`;
}
/**

View File

@@ -124,8 +124,6 @@ export default class SpecialEntityCard extends Vue {
handleClick(): void {
if (this.selectable && !this.conflicted) {
this.emitEntitySelected({
type: "special",
entityType: this.entityType,
data: this.entityData,
});
} else if (this.conflicted && this.notify) {
@@ -145,13 +143,7 @@ export default class SpecialEntityCard extends Vue {
// Emit methods using @Emit decorator
@Emit("entity-selected")
emitEntitySelected(data: {
type: string;
entityType: string;
data: { did?: string; name: string };
}): {
type: string;
entityType: string;
emitEntitySelected(data: { data: { did?: string; name: string } }): {
data: { did?: string; name: string };
} {
return data;

14
src/constants/entities.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* Constants for entity-related strings, particularly for unnamed/unknown person entities
*/
// Core unnamed entity names
export const UNNAMED_ENTITY_NAME = "Unnamed";
// Descriptive phrases for unnamed entities
export const SOMEONE_UNNAMED = "Someone Unnamed";
export const THAT_UNNAMED_PERSON = "That unnamed person";
export const UNNAMED_PERSON = "unnamed person";
// Project-related unnamed entities
export const UNNAMED_PROJECT = "Unnamed Project";

View File

@@ -1,4 +1,5 @@
import axios from "axios";
import { THAT_UNNAMED_PERSON } from "./entities";
// Notification message constants for user-facing notifications
// Add new notification messages here as needed
@@ -873,7 +874,7 @@ export const NOTIFY_CONTACT_LINK_COPIED = {
// Template for registration success message
// Used in: ContactsView.vue (register method - registration success with contact name)
export const getRegisterPersonSuccessMessage = (name?: string): string =>
`${name || "That unnamed person"} ${NOTIFY_REGISTER_PERSON_SUCCESS.message}`;
`${name || THAT_UNNAMED_PERSON} ${NOTIFY_REGISTER_PERSON_SUCCESS.message}`;
// Template for visibility success message
// Used in: ContactsView.vue (setVisibility method - visibility success with contact name)
@@ -1378,7 +1379,7 @@ export function createQRContactAddedMessage(hasVisibility: boolean): string {
export function createQRRegistrationSuccessMessage(
contactName: string,
): string {
return `${contactName || "That unnamed person"}${NOTIFY_QR_REGISTRATION_SUCCESS.message}`;
return `${contactName || THAT_UNNAMED_PERSON}${NOTIFY_QR_REGISTRATION_SUCCESS.message}`;
}
// ContactQRScanShowView.vue timeout constants

View File

@@ -60,6 +60,7 @@ import { PlanSummaryRecord } from "../interfaces/records";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { APP_SERVER } from "@/constants/app";
import { SOMEONE_UNNAMED } from "@/constants/entities";
/**
* Standard context for schema.org data
@@ -309,7 +310,7 @@ export function didInfoForContact(
showDidForVisible: boolean = false,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): { known: boolean; displayName: string; profileImageUrl?: string } {
if (!did) return { displayName: "Someone Not Named", known: false };
if (!did) return { displayName: SOMEONE_UNNAMED, known: false };
if (did === activeDid) {
return { displayName: "You", known: true };
} else if (contact) {

View File

@@ -33,6 +33,7 @@ import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { IIdentifier } from "@veramo/core";
import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
import { UNNAMED_PERSON } from "@/constants/entities";
// Consolidate this with src/utils/PlatformServiceMixin.mapQueryResultToValues
function mapQueryResultToValues(
@@ -192,7 +193,7 @@ export const nameForContact = (
): string => {
return (
(contact?.name as string) ||
(capitalize ? "This" : "this") + " unnamed user"
(capitalize ? "This" : "this") + " " + UNNAMED_PERSON
);
};

View File

@@ -74,13 +74,13 @@ export class DeepLinkHandler {
* @throws {DeepLinkError} If validation fails or route is invalid
*/
async handleDeepLink(url: string): Promise<void> {
logger.info(`[DeepLink] 🚀 Starting deeplink processing for URL: ${url}`);
logger.debug(`[DeepLink] 🚀 Starting deeplink processing for URL: ${url}`);
try {
logger.info(`[DeepLink] 📍 Parsing URL: ${url}`);
logger.debug(`[DeepLink] 📍 Parsing URL: ${url}`);
const { path, params, query } = this.parseDeepLink(url);
logger.info(`[DeepLink] ✅ URL parsed successfully:`, {
logger.debug(`[DeepLink] ✅ URL parsed successfully:`, {
path,
params: Object.keys(params),
query: Object.keys(query),
@@ -93,10 +93,10 @@ export class DeepLinkHandler {
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
);
logger.info(`[DeepLink] 🧹 Parameters sanitized:`, sanitizedParams);
logger.debug(`[DeepLink] 🧹 Parameters sanitized:`, sanitizedParams);
await this.validateAndRoute(path, sanitizedParams, query);
logger.info(`[DeepLink] 🎯 Deeplink processing completed successfully`);
logger.debug(`[DeepLink] 🎯 Deeplink processing completed successfully`);
} catch (error) {
logger.error(`[DeepLink] ❌ Deeplink processing failed:`, {
url,
@@ -159,7 +159,7 @@ export class DeepLinkHandler {
logger.debug(`[DeepLink] 🔗 Query parameters extracted:`, query);
}
logger.info(`[DeepLink] ✅ Parse completed:`, {
logger.debug(`[DeepLink] ✅ Parse completed:`, {
routePath,
pathParams: pathParams.length,
queryParams: Object.keys(query).length,
@@ -186,7 +186,7 @@ export class DeepLinkHandler {
params: Record<string, string>,
query: Record<string, string>,
): Promise<void> {
logger.info(
logger.debug(
`[DeepLink] 🎯 Starting validation and routing for path: ${path}`,
);
@@ -197,11 +197,11 @@ export class DeepLinkHandler {
logger.debug(`[DeepLink] 🔍 Validating route path: ${path}`);
// Validate route exists
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
logger.info(`[DeepLink] ✅ Route validation passed: ${validRoute}`);
logger.debug(`[DeepLink] ✅ Route validation passed: ${validRoute}`);
// Get route configuration
const routeConfig = ROUTE_MAP[validRoute];
logger.info(`[DeepLink] 📋 Route config retrieved:`, routeConfig);
logger.debug(`[DeepLink] 📋 Route config retrieved:`, routeConfig);
if (!routeConfig) {
logger.error(`[DeepLink] ❌ No route config found for: ${validRoute}`);
@@ -209,7 +209,7 @@ export class DeepLinkHandler {
}
routeName = routeConfig.name;
logger.info(`[DeepLink] 🎯 Route name resolved: ${routeName}`);
logger.debug(`[DeepLink] 🎯 Route name resolved: ${routeName}`);
} catch (error) {
logger.error(`[DeepLink] ❌ Route validation failed:`, {
path,
@@ -228,14 +228,14 @@ export class DeepLinkHandler {
},
});
logger.info(
logger.debug(
`[DeepLink] 🔄 Redirected to error page for invalid route: ${path}`,
);
return;
}
// Continue with parameter validation
logger.info(
logger.debug(
`[DeepLink] 🔍 Starting parameter validation for route: ${routeName}`,
);
@@ -258,7 +258,7 @@ export class DeepLinkHandler {
if (pathSchema) {
logger.debug(`[DeepLink] 🔍 Validating path parameters:`, params);
validatedPathParams = await pathSchema.parseAsync(params);
logger.info(
logger.debug(
`[DeepLink] ✅ Path parameters validated:`,
validatedPathParams,
);
@@ -270,7 +270,7 @@ export class DeepLinkHandler {
if (querySchema) {
logger.debug(`[DeepLink] 🔍 Validating query parameters:`, query);
validatedQueryParams = await querySchema.parseAsync(query);
logger.info(
logger.debug(
`[DeepLink] ✅ Query parameters validated:`,
validatedQueryParams,
);
@@ -299,7 +299,7 @@ export class DeepLinkHandler {
},
});
logger.info(
logger.debug(
`[DeepLink] 🔄 Redirected to error page for invalid parameters`,
);
return;
@@ -307,7 +307,7 @@ export class DeepLinkHandler {
// Attempt navigation
try {
logger.info(`[DeepLink] 🚀 Attempting navigation:`, {
logger.debug(`[DeepLink] 🚀 Attempting navigation:`, {
routeName,
pathParams: validatedPathParams,
queryParams: validatedQueryParams,
@@ -319,7 +319,7 @@ export class DeepLinkHandler {
query: validatedQueryParams,
});
logger.info(`[DeepLink] ✅ Navigation successful to: ${routeName}`);
logger.debug(`[DeepLink] ✅ Navigation successful to: ${routeName}`);
} catch (error) {
logger.error(`[DeepLink] ❌ Navigation failed:`, {
routeName,
@@ -342,7 +342,7 @@ export class DeepLinkHandler {
},
});
logger.info(
logger.debug(
`[DeepLink] 🔄 Redirected to error page for navigation failure`,
);
}

View File

@@ -17,20 +17,40 @@
<!-- Results List -->
<ul class="border-t border-slate-300">
<li class="border-b border-slate-300 py-3">
<!-- "You" entity -->
<li v-if="shouldShowYouEntity" class="border-b border-slate-300 py-3">
<h2 class="text-base flex gap-4 items-center">
<span class="grow flex gap-2 items-center font-medium">
<font-awesome
icon="circle-question"
class="text-slate-400 text-4xl"
/>
<span class="italic text-slate-400">(Not Named)</span>
<font-awesome icon="hand" class="text-blue-500 text-4xl shrink-0" />
<span class="text-ellipsis overflow-hidden text-blue-500">You</span>
</span>
<span class="text-right">
<button
type="button"
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="openDialog('Unnamed')"
@click="openDialog({ did: activeDid, name: 'You' })"
>
<font-awesome icon="gift" class="fa-fw"></font-awesome>
</button>
</span>
</h2>
</li>
<li class="border-b border-slate-300 py-3">
<h2 class="text-base flex gap-4 items-center">
<span class="grow flex gap-2 items-center font-medium">
<font-awesome
icon="circle-question"
class="text-slate-400 text-4xl shrink-0"
/>
<span class="text-ellipsis overflow-hidden italic text-slate-500">{{
unnamedEntityName
}}</span>
</span>
<span class="text-right">
<button
type="button"
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="openDialog({ did: '', name: unnamedEntityName })"
>
<font-awesome icon="gift" class="fa-fw"></font-awesome>
</button>
@@ -43,14 +63,22 @@
class="border-b border-slate-300 py-3"
>
<h2 class="text-base flex gap-4 items-center">
<span class="grow flex gap-2 items-center font-medium">
<span
class="grow flex gap-2 items-center font-medium overflow-hidden"
>
<EntityIcon
:contact="contact"
:icon-size="34"
class="inline-block align-middle border border-slate-300 rounded-full overflow-hidden"
class="inline-block align-middle border border-slate-300 rounded-full overflow-hidden shrink-0"
/>
<span v-if="contact.name">{{ contact.name }}</span>
<span v-else class="italic text-slate-400">(No name)</span>
<span v-if="contact.name" class="text-ellipsis overflow-hidden">{{
contact.name
}}</span>
<span
v-else
class="text-ellipsis overflow-hidden italic text-slate-500"
>{{ contact.did }}</span
>
</span>
<span class="text-right">
<button
@@ -72,6 +100,7 @@
:from-project-id="fromProjectId"
:to-project-id="toProjectId"
:is-from-project-view="isFromProjectView"
:hide-show-all="true"
/>
</section>
</template>
@@ -89,6 +118,7 @@ import { GiverReceiverInputInfo } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
@Component({
components: { GiftedDialog, QuickNav, EntityIcon },
mixins: [PlatformServiceMixin],
@@ -188,147 +218,151 @@ export default class ContactGiftingView extends Vue {
}
}
openDialog(contact?: GiverReceiverInputInfo | "Unnamed") {
if (contact === "Unnamed") {
// Special case: Handle "Unnamed" contacts for both givers and recipients
let recipient: GiverReceiverInputInfo;
let giver: GiverReceiverInputInfo | undefined;
openDialog(contact?: GiverReceiverInputInfo) {
// Determine the selected entity based on contact type
const selectedEntity = this.createEntityFromContact(contact);
if (this.stepType === "giver") {
// We're selecting a giver, so preserve the existing recipient from context
if (this.recipientEntityType === "project") {
recipient = {
did: this.recipientProjectHandleId,
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
// Preserve the existing recipient from context
if (this.recipientDid === this.activeDid) {
// Recipient was "You"
recipient = { did: this.activeDid, name: "You" };
} else if (this.recipientDid) {
// Recipient was a regular contact
recipient = {
did: this.recipientDid,
name: this.recipientProjectName || "Someone",
};
} else {
// Fallback to "You" if no recipient was previously selected
recipient = { did: this.activeDid, name: "You" };
}
}
giver = undefined; // Will be set to "Unnamed" in GiftedDialog
} else {
// We're selecting a recipient, so recipient is "Unnamed" and giver is preserved from context
recipient = { did: "", name: "Unnamed" };
// Create giver and recipient based on step type and selected entity
const { giver, recipient } = this.createGiverAndRecipient(selectedEntity);
// Preserve the existing giver from the context
if (this.giverEntityType === "project") {
giver = {
did: this.giverProjectHandleId,
name: this.giverProjectName,
image: this.giverProjectImage,
handleId: this.giverProjectHandleId,
};
} else if (this.giverDid) {
giver = {
did: this.giverDid,
name: this.giverProjectName || "Someone",
};
} else {
giver = { did: this.activeDid, name: "You" };
}
}
// Open the dialog
(this.$refs.giftedDialog as GiftedDialog).open(
giver,
recipient,
this.offerId,
this.prompt,
this.description,
this.amountInput,
this.unitCode,
);
(this.$refs.giftedDialog as GiftedDialog).open(
giver,
recipient,
this.offerId,
this.prompt,
this.description,
this.amountInput,
this.unitCode,
);
// Move to Step 2 - entities are already set by the open() call
(this.$refs.giftedDialog as GiftedDialog).moveToStep2();
}
// Move to Step 2 - entities are already set by the open() call
(this.$refs.giftedDialog as GiftedDialog).moveToStep2();
/**
* Creates an entity object from the contact parameter
* Uses DID-based logic to determine "You" and "Unnamed" entities
*/
private createEntityFromContact(
contact?: GiverReceiverInputInfo,
): GiverReceiverInputInfo | undefined {
if (!contact) {
return undefined;
}
// Handle GiverReceiverInputInfo object
if (contact.did === this.activeDid) {
// If DID matches active DID, create "You" entity
return { did: this.activeDid, name: "You" };
} else if (!contact.did || contact.did === "") {
// If DID is empty/null, create "Unnamed" entity
return { did: "", name: UNNAMED_ENTITY_NAME };
} else {
// Regular case: contact is a GiverReceiverInputInfo
let giver: GiverReceiverInputInfo;
let recipient: GiverReceiverInputInfo;
// Create a copy of the contact to avoid modifying the original
return { ...contact };
}
}
if (this.stepType === "giver") {
// We're selecting a giver, so the contact becomes the giver
giver = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
/**
* Creates giver and recipient objects based on step type and selected entity
*/
private createGiverAndRecipient(selectedEntity?: GiverReceiverInputInfo): {
giver: GiverReceiverInputInfo | undefined;
recipient: GiverReceiverInputInfo;
} {
if (this.stepType === "giver") {
// We're selecting a giver, so the selected entity becomes the giver
const giver = selectedEntity;
const recipient = this.createRecipientFromContext();
return { giver, recipient };
} else {
// We're selecting a recipient, so the selected entity becomes the recipient
const recipient = selectedEntity || {
did: "",
name: UNNAMED_ENTITY_NAME,
};
const giver = this.createGiverFromContext();
return { giver, recipient };
}
}
// Preserve the existing recipient from the context
if (this.recipientEntityType === "project") {
recipient = {
did: this.recipientProjectHandleId,
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
// Check if the preserved recipient was "You" or a regular contact
if (this.recipientDid === this.activeDid) {
// Recipient was "You"
recipient = { did: this.activeDid, name: "You" };
} else if (this.recipientDid) {
// Recipient was a regular contact
recipient = {
did: this.recipientDid,
name: this.recipientProjectName || "Someone",
};
} else {
// Fallback to "Unnamed"
recipient = { did: "", name: "Unnamed" };
}
}
/**
* Creates recipient object from context (preserves existing recipient)
*/
private createRecipientFromContext(): GiverReceiverInputInfo {
if (this.recipientEntityType === "project") {
return {
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
if (this.recipientDid === this.activeDid) {
return { did: this.activeDid, name: "You" };
} else if (this.recipientDid) {
return {
did: this.recipientDid,
name: this.recipientProjectName,
};
} else {
// We're selecting a recipient, so the contact becomes the recipient
recipient = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
// Preserve the existing giver from the context
if (this.giverEntityType === "project") {
giver = {
did: this.giverProjectHandleId,
name: this.giverProjectName,
image: this.giverProjectImage,
handleId: this.giverProjectHandleId,
};
} else {
// Check if the preserved giver was "You" or a regular contact
if (this.giverDid === this.activeDid) {
// Giver was "You"
giver = { did: this.activeDid, name: "You" };
} else if (this.giverDid) {
// Giver was a regular contact
giver = {
did: this.giverDid,
name: this.giverProjectName || "Someone",
};
} else {
// Fallback to "Unnamed"
giver = { did: "", name: "Unnamed" };
}
}
return { did: "", name: UNNAMED_ENTITY_NAME };
}
}
}
(this.$refs.giftedDialog as GiftedDialog).open(
giver,
recipient,
this.offerId,
this.prompt,
this.description,
this.amountInput,
this.unitCode,
);
/**
* Creates giver object from context (preserves existing giver)
*/
private createGiverFromContext(): GiverReceiverInputInfo {
if (this.giverEntityType === "project") {
return {
name: this.giverProjectName,
image: this.giverProjectImage,
handleId: this.giverProjectHandleId,
};
} else {
if (this.giverDid === this.activeDid) {
return { did: this.activeDid, name: "You" };
} else if (this.giverDid) {
return {
did: this.giverDid,
name: this.giverProjectName,
};
} else {
return { did: "", name: UNNAMED_ENTITY_NAME };
}
}
}
// Move to Step 2 - entities are already set by the open() call
(this.$refs.giftedDialog as GiftedDialog).moveToStep2();
/**
* Get the unnamed entity name constant
*/
get unnamedEntityName(): string {
return UNNAMED_ENTITY_NAME;
}
get shouldShowYouEntity(): boolean {
if (this.stepType === "giver") {
// When selecting a giver, show "You" if the current recipient is not "You"
// This prevents selecting yourself as both giver and recipient
if (this.recipientEntityType === "project") {
// If recipient is a project, we can select "You" as giver
return true;
} else {
// If recipient is a person, check if it's not "You"
return this.recipientDid !== this.activeDid;
}
} else {
// When selecting a recipient, show "You" if the current giver is not "You"
// This prevents selecting yourself as both giver and recipient
if (this.giverEntityType === "project") {
// If giver is a project, we can select "You" as recipient
return true;
} else {
// If giver is a person, check if it's not "You"
return this.giverDid !== this.activeDid;
}
}
}
}

View File

@@ -290,6 +290,7 @@ import {
NOTIFY_SERVER_ACCESS_ERROR,
NOTIFY_NO_IDENTITY_ERROR,
} from "@/constants/notifications";
import { THAT_UNNAMED_PERSON } from "@/constants/entities";
/**
* DIDView Component
@@ -551,7 +552,7 @@ export default class DIDView extends Vue {
contact.registered = true;
await this.$updateContact(contact.did, { registered: true });
const name = contact.name || "That unnamed person";
const name = contact.name || THAT_UNNAMED_PERSON;
this.notify.success(
`${name} ${NOTIFY_REGISTRATION_SUCCESS.message}`,
TIMEOUTS.LONG,

View File

@@ -233,7 +233,7 @@
<div class="grow">
<h2 class="text-base font-semibold">
{{ project.name || "Unnamed Project" }}
{{ project.name || unnamedProject }}
</h2>
<div class="text-sm">
<font-awesome
@@ -340,6 +340,7 @@ import {
NOTIFY_DISCOVER_LOCAL_SEARCH_ERROR,
NOTIFY_DISCOVER_MAP_SEARCH_ERROR,
} from "@/constants/notifications";
import { UNNAMED_PROJECT } from "@/constants/entities";
interface Tile {
indexLat: number;
indexLon: number;
@@ -370,6 +371,13 @@ export default class DiscoverView extends Vue {
notify!: ReturnType<typeof createNotifyHelpers>;
/**
* Get the unnamed project constant
*/
get unnamedProject(): string {
return UNNAMED_PROJECT;
}
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];

View File

@@ -200,7 +200,7 @@
</p>
<p>
Then you can record your appreciation for... whatever: select any contact on the home page
(or "Unnamed") and send it. The main goal is to record what people
(or "{{ unnamedEntityName }}") and send it. The main goal is to record what people
have given you, to grow giving economies. You can also record your own
ideas for projects. Each claim is recorded on a
custom ledger.
@@ -600,6 +600,7 @@ import QuickNav from "../components/QuickNav.vue";
import { APP_SERVER } from "../constants/app";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { QRNavigationService } from "@/services/QRNavigationService";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
/**
* HelpView.vue - Comprehensive Help System Component
@@ -647,6 +648,13 @@ export default class HelpView extends Vue {
APP_SERVER = APP_SERVER;
// Capacitor reference removed - using QRNavigationService instead
/**
* Get the unnamed entity name constant
*/
get unnamedEntityName(): string {
return UNNAMED_ENTITY_NAME;
}
// Ideally, we put no functionality in here, especially in the setup,
// because we never want this page to have a chance of throwing an error.

View File

@@ -282,6 +282,7 @@ import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NOTIFY_CONTACT_LOADING_ISSUE } from "@/constants/notifications";
import * as Package from "../../package.json";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
// consolidate this with GiveActionClaim in src/interfaces/claims.ts
interface Claim {
@@ -1546,30 +1547,41 @@ export default class HomeView extends Vue {
* @param giver Optional contact info for giver
* @param description Optional gift description
*/
openDialog(giver?: GiverReceiverInputInfo | "Unnamed", prompt?: string) {
if (giver === "Unnamed") {
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
(this.$refs.giftedDialog as GiftedDialog).open(
undefined,
{
did: this.activeDid,
name: "You",
} as GiverReceiverInputInfo,
undefined,
prompt,
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.giftedDialog as GiftedDialog).selectGiver();
openDialog(giver?: GiverReceiverInputInfo, prompt?: string) {
// Determine the giver entity based on DID logic
const giverEntity = this.createGiverEntity(giver);
(this.$refs.giftedDialog as GiftedDialog).open(
giverEntity,
{
did: this.activeDid,
name: "You", // In HomeView, we always use "You" as the giver
} as GiverReceiverInputInfo,
undefined,
prompt,
);
}
/**
* Creates giver entity using DID-based logic
*/
private createGiverEntity(
giver?: GiverReceiverInputInfo,
): GiverReceiverInputInfo | undefined {
if (!giver) {
return undefined;
}
// Handle GiverReceiverInputInfo object
if (giver.did === this.activeDid) {
// If DID matches active DID, create "You" entity
return { did: this.activeDid, name: "You" };
} else if (!giver.did || giver.did === "") {
// If DID is empty/null, create "Unnamed" entity
return { did: "", name: UNNAMED_ENTITY_NAME };
} else {
(this.$refs.giftedDialog as GiftedDialog).open(
giver,
{
did: this.activeDid,
name: "You",
} as GiverReceiverInputInfo,
undefined,
prompt,
);
// Return the giver as-is
return giver;
}
}
@@ -1632,10 +1644,15 @@ export default class HomeView extends Vue {
this.isImageViewerOpen = true;
}
openPersonDialog(
giver?: GiverReceiverInputInfo | "Unnamed",
prompt?: string,
) {
private handleQRCodeClick() {
if (Capacitor.isNativePlatform()) {
this.$router.push({ name: "contact-qr-scan-full" });
} else {
this.$router.push({ name: "contact-qr" });
}
}
openPersonDialog(giver?: GiverReceiverInputInfo, prompt?: string) {
this.showProjectsDialog = false;
this.openDialog(giver, prompt);
}

View File

@@ -183,7 +183,7 @@
class="text-blue-500"
@click="onClickLoadProject(plan.handleId)"
>
{{ plan.name || "Unnamed Project" }}
{{ plan.name || unnamedProject }}
</button>
</div>
<div v-if="fulfillersToHitLimit" class="text-center">
@@ -207,7 +207,7 @@
class="text-blue-500"
@click="onClickLoadProject(fulfilledByThis.handleId)"
>
{{ fulfilledByThis.name || "Unnamed Project" }}
{{ fulfilledByThis.name || unnamedProject }}
</button>
</div>
</div>
@@ -611,6 +611,7 @@ import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NOTIFY_CONFIRM_CLAIM } from "@/constants/notifications";
import { APP_SERVER } from "@/constants/app";
import { UNNAMED_PROJECT } from "@/constants/entities";
/**
* Project View Component
* @author Matthew Raymer
@@ -664,6 +665,13 @@ export default class ProjectViewView extends Vue {
/** Notification helpers instance */
notify!: ReturnType<typeof createNotifyHelpers>;
/**
* Get the unnamed project constant
*/
get unnamedProject(): string {
return UNNAMED_PROJECT;
}
// Account and Settings State
/** Currently active DID */
activeDid = "";

View File

@@ -244,7 +244,7 @@
<div class="grow overflow-hidden">
<h2 class="text-base font-semibold">
{{ project.name || "Unnamed Project" }}
{{ project.name || unnamedProject }}
</h2>
<div class="text-sm truncate">
{{ project.description }}
@@ -286,6 +286,7 @@ import {
NOTIFY_OFFERS_LOAD_ERROR,
NOTIFY_OFFERS_FETCH_ERROR,
} from "@/constants/notifications";
import { UNNAMED_PROJECT } from "@/constants/entities";
/**
* Projects View Component
@@ -324,6 +325,13 @@ export default class ProjectsView extends Vue {
notify!: ReturnType<typeof createNotifyHelpers>;
/**
* Get the unnamed project constant
*/
get unnamedProject(): string {
return UNNAMED_PROJECT;
}
// User account state
activeDid = "";
allContacts: Array<Contact> = [];

View File

@@ -239,7 +239,8 @@ export default class QuickActionBvcEndView extends Vue {
}
const eventStartDateObj = currentOrPreviousSat
.set({ weekday: 6 })
.set({ hour: 9 })
.set({ hour: 8 })
.set({ minute: 30 }) // to catch if people put their claims 30 minutes early
.startOf("hour");
// Hack, but full ISO pushes the length to 340 which crashes verifyJWT!

View File

@@ -69,6 +69,7 @@
*/
import { test, expect } from '@playwright/test';
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUtils';
test('Check activity feed - check that server is running', async ({ page }) => {
@@ -177,7 +178,7 @@ test('Check User 0 can register a random person', async ({ page }) => {
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
await page.getByRole('button', { name: 'Person' }).click();
await page.getByRole('listitem').filter({ hasText: 'Unnamed' }).locator('svg').click();
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
await page.getByPlaceholder('What was given').fill('Gave me access!');
await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That gift was recorded.')).toBeVisible();

View File

@@ -79,6 +79,7 @@
* ```
*/
import { test, expect } from '@playwright/test';
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
import { importUser } from './testUtils';
test('Record something given', async ({ page }) => {
@@ -101,7 +102,7 @@ test('Record something given', async ({ page }) => {
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
await page.getByRole('button', { name: 'Person' }).click();
await page.getByRole('listitem').filter({ hasText: 'Unnamed' }).locator('svg').click();
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
await page.getByPlaceholder('What was given').fill(finalTitle);
await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString());
await page.getByRole('button', { name: 'Sign & Send' }).click();

View File

@@ -85,6 +85,7 @@
*/
import { test, expect } from '@playwright/test';
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
import { importUser, createUniqueStringsArray, createRandomNumbersArray } from './testUtils';
test('Record 9 new gifts', async ({ page }) => {
@@ -116,7 +117,7 @@ test('Record 9 new gifts', async ({ page }) => {
await page.getByTestId('closeOnboardingAndFinish').click();
}
await page.getByRole('button', { name: 'Person' }).click();
await page.getByRole('listitem').filter({ hasText: 'Unnamed' }).locator('svg').click();
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
await page.getByPlaceholder('What was given').fill(finalTitles[i]);
await page.getByRole('spinbutton').fill(finalNumbers[i].toString());
await page.getByRole('button', { name: 'Sign & Send' }).click();

View File

@@ -23,11 +23,36 @@ test('New offers for another user', async ({ page }) => {
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(autoCreatedDid + ', A Friend');
await expect(page.locator('button > svg.fa-plus')).toBeVisible();
await page.locator('button > svg.fa-plus').click();
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
await page.locator('div[role="alert"]:has-text("Register") button:has-text("No")').click(); // don't register
await expect(page.locator('div[role="alert"] h4:has-text("Success")')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
// Wait for any dialogs to appear and dismiss them systematically
await page.waitForTimeout(1000); // Give time for any dialogs to appear
// Check for and dismiss export dialog
const exportDialog = page.locator('div[role="alert"]:has-text("Export Your Data")');
if (await exportDialog.isVisible()) {
await page.locator('div[role="alert"]:has-text("Export Your Data") button:has-text("No, Not Now")').click();
await expect(exportDialog).toBeHidden();
await page.waitForTimeout(500);
}
// Check for any other modal overlays that might be blocking
const modalOverlay = page.locator('div[class*="bg-slate-900"]');
if (await modalOverlay.isVisible()) {
// Try to find and click any close buttons in the overlay
const closeButtons = page.locator('div[class*="bg-slate-900"] button');
if (await closeButtons.count() > 0) {
await closeButtons.first().click();
await page.waitForTimeout(500);
}
}
// Ensure no dialogs are blocking before proceeding
await page.waitForTimeout(1000);
// show buttons to make offers directly to people
await page.getByRole('button').filter({ hasText: /See Actions/i }).click();
@@ -39,8 +64,8 @@ test('New offers for another user', async ({ page }) => {
await page.getByTestId('inputOfferAmount').locator('input').fill('1');
await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That offer was recorded.')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
await page.locator('div[role="alert"]:has-text("That offer was recorded") button > svg.fa-xmark').click(); // dismiss info alert
await expect(page.locator('div[role="alert"]:has-text("That offer was recorded") button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
// make another offer to user 1
const randomString2 = Math.random().toString(36).substring(2, 5);
@@ -49,8 +74,8 @@ test('New offers for another user', async ({ page }) => {
await page.getByTestId('inputOfferAmount').locator('input').fill('3');
await page.getByRole('button', { name: 'Sign & Send' }).click();
await expect(page.getByText('That offer was recorded.')).toBeVisible();
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
await page.locator('div[role="alert"]:has-text("That offer was recorded") button > svg.fa-xmark').click(); // dismiss info alert
await expect(page.locator('div[role="alert"]:has-text("That offer was recorded") button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
// Switch back to the auto-created DID (the "another user") to see the offers
await switchToUser(page, autoCreatedDid);

View File

@@ -157,11 +157,11 @@ export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
.getByPlaceholder("URL or DID, Name, Public Key")
.fill(`${newDid}, ${contactName}`);
await page.locator("button > svg.fa-plus").click();
// register them
await page.locator('div[role="alert"] button:has-text("Yes")').click();
// register them - be more specific to avoid multiple button matches
await page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes")').click();
// wait for it to disappear because the next steps may depend on alerts being gone
await expect(
page.locator('div[role="alert"] button:has-text("Yes")')
page.locator('div[role="alert"]:has-text("Register") button:has-text("Yes")')
).toBeHidden();
await expect(page.locator("li", { hasText: contactName })).toBeVisible();