diff --git a/.gitignore b/.gitignore index cc74ddc..40cc7d0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /dist .DS_Store /.tanstack +/tmp +/.codegen-cache/ diff --git a/biome.json b/biome.json index c90a82e..b3568d9 100644 --- a/biome.json +++ b/biome.json @@ -16,7 +16,8 @@ "tsconfig.json", "tsconfig.node.json", "vite.config.ts", - "!src/**/routeTree.gen.ts" + "!src/**/routeTree.gen.ts", + "scripts/**" ], "ignoreUnknown": false }, diff --git a/package.json b/package.json index 5236fc5..72f96f8 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,15 @@ "lint:fix": "biome check --write", "test": "echo \"Error: no test specified\" && exit 1", "all": "pnpm format && pnpm typecheck && biome check --write --diagnostic-level=error", - "hooks": "cp .hooks/* .git/hooks/" + "hooks": "cp .hooks/* .git/hooks/", + "generate-types": "pnpm exec tsx scripts/generate-types.ts" }, "keywords": [], "author": "Health Samurai", "license": "MIT", - "packageManager": "pnpm@10.14.0", + "packageManager": "pnpm@10.22.0", "devDependencies": { + "@atomic-ehr/codegen": "canary", "@biomejs/biome": "2.1.3", "@tailwindcss/vite": "^4.1.12", "@tanstack/router-plugin": "^1.131.13", @@ -36,6 +38,10 @@ "vite": "^7.1.2" }, "dependencies": { + "@git-diff-view/core": "^0.0.30", + "@git-diff-view/file": "^0.0.30", + "@git-diff-view/react": "^0.0.30", + "@health-samurai/aidbox-client": "0.0.0-alpha.3", "@health-samurai/react-components": "^0.0.0-alpha.10", "@tanstack/react-query": "^5.85.3", "@tanstack/react-query-devtools": "^5.85.3", @@ -49,9 +55,6 @@ "lucide-react": "^0.539.0", "react": "^19.1.1", "react-dom": "^19.1.1", - "sql-formatter": "^15.6.9", - "@git-diff-view/core": "^0.0.30", - "@git-diff-view/file": "^0.0.30", - "@git-diff-view/react": "^0.0.30" + "sql-formatter": "^15.6.9" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59d1c2d..d77c0c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@git-diff-view/react': specifier: ^0.0.30 version: 0.0.30(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@health-samurai/aidbox-client': + specifier: 0.0.0-alpha.3 + version: 0.0.0-alpha.3 '@health-samurai/react-components': specifier: ^0.0.0-alpha.10 version: 0.0.0-alpha.10(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0) @@ -31,7 +34,7 @@ importers: version: 1.132.23(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@tanstack/react-router-devtools': specifier: ^1.131.13 - version: 1.132.23(@tanstack/react-router@1.132.23(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.132.21)(csstype@3.1.3)(jiti@2.6.0)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.9)(tiny-invariant@1.3.3)(tsx@4.20.6) + version: 1.132.23(@tanstack/react-router@1.132.23(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.132.21)(csstype@3.1.3)(jiti@2.6.0)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.9)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.2) '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 @@ -60,15 +63,18 @@ importers: specifier: ^15.6.9 version: 15.6.9 devDependencies: + '@atomic-ehr/codegen': + specifier: canary + version: 0.0.2-canary.20251117105828.24a32f4(typescript@5.9.2) '@biomejs/biome': specifier: 2.1.3 version: 2.1.3 '@tailwindcss/vite': specifier: ^4.1.12 - version: 4.1.13(vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)) + version: 4.1.13(vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.2)) '@tanstack/router-plugin': specifier: ^1.131.13 - version: 1.132.23(@tanstack/react-router@1.132.23(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)) + version: 1.132.23(@tanstack/react-router@1.132.23(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.2)) '@types/css': specifier: ^0.0.38 version: 0.0.38 @@ -83,7 +89,7 @@ importers: version: 19.2.0(@types/react@19.2.0) '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)) + version: 4.7.0(vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.2)) tailwindcss: specifier: ^4.1.12 version: 4.1.13 @@ -95,10 +101,25 @@ importers: version: 5.9.2 vite: specifier: ^7.1.2 - version: 7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6) + version: 7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.2) packages: + '@atomic-ehr/codegen@0.0.2-canary.20251117105828.24a32f4': + resolution: {integrity: sha512-X/MaL9doKmCYYcHSmxtzSPq3c8v6poLM/ficyg11Z7zA7uoXLjK3FbP3I9uEGAxg5rBQaDLsMOODqh/EwlFJFA==} + hasBin: true + + '@atomic-ehr/fhir-canonical-manager@0.0.15': + resolution: {integrity: sha512-hVtvJrs7NjSuDx8RiUpEglaF3PuKESGRNslvEtsIl6cThnwSdovTCo+LgKD8u3W0aMdq0Dpbah9ISvkeB9+ASw==} + hasBin: true + peerDependencies: + typescript: ^5 + + '@atomic-ehr/fhirschema@0.0.5': + resolution: {integrity: sha512-B/8ScNnnQUIR6d3FsIuGGvanOyE2j7W3mAubVmpPE2I/tho+meEBRrGgs5E+AY4jDz9mviTdOta08RpIhH2kew==} + peerDependencies: + typescript: ^5 + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -526,6 +547,9 @@ packages: react: '*' react-dom: '*' + '@health-samurai/aidbox-client@0.0.0-alpha.3': + resolution: {integrity: sha512-5TjsrYZ/pObmkX6ILp7RV5SegFRJmfxyVJa1V+E2XiEZA3/hMl5p5/gPUQEZTk2eZvhl2ygZ4dpwK5GIsWPsGQ==} + '@health-samurai/react-components@0.0.0-alpha.10': resolution: {integrity: sha512-qOSASA7b8wL2FM+bjnisHXgaJQ4QWqh4M19KAl3tJJj7fuFVaDSjaD4ORxmC4xncqpEZGqqMtIekDiHrFmgC2A==} @@ -534,6 +558,140 @@ packages: peerDependencies: react-hook-form: ^7.55.0 + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -1642,6 +1800,9 @@ packages: '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/json-patch@0.0.33': + resolution: {integrity: sha512-XQ9hIoJCtnvTCnIV+p+SWQbY8Bt1pe+RuTkPG7lQeRCa9sPwYbhb0aYzM2paVPk0SGvV70R33rxjPjBcJ4Kdxw==} + '@types/react-dom@19.2.0': resolution: {integrity: sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==} peerDependencies: @@ -1670,6 +1831,25 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + ansis@4.2.0: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} @@ -1715,6 +1895,13 @@ packages: caniuse-lite@1.0.30001745: resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1726,6 +1913,22 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1736,6 +1939,13 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -1857,6 +2067,12 @@ packages: embla-carousel@8.6.0: resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} @@ -1878,6 +2094,9 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} @@ -1889,6 +2108,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1914,6 +2136,14 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -1933,10 +2163,19 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + highlight.js@11.11.1: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + input-otp@1.4.2: resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} peerDependencies: @@ -1955,14 +2194,30 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + isbot@5.1.31: resolution: {integrity: sha512-DPgQshehErHAqSCKDb3rNW03pa2wS/v5evvUqtxt6TTnHRqAG8FdzcSSJs9656pK6Y+NT7K9R4acEYXLHYfpUQ==} engines: {node: '>=18'} @@ -1987,6 +2242,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -2059,6 +2317,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2085,6 +2347,13 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -2099,6 +2368,10 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2108,6 +2381,9 @@ packages: resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} hasBin: true + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -2125,6 +2401,14 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2276,9 +2560,17 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + ret@0.1.15: resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} engines: {node: '>=0.12'} @@ -2295,6 +2587,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -2312,6 +2607,10 @@ packages: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + solid-js@1.9.9: resolution: {integrity: sha512-A0ZBPJQldAeGCTW0YRYJmt7RCeh5rbFfPZ2aOttgYnctHE7HgKeHCBB/PVc2P7eOfmNXqMFFFoYYdm3S4dcbkA==} @@ -2337,6 +2636,26 @@ packages: resolution: {integrity: sha512-r9VKnkRfKW7jbhTgytwbM+JqmFclQYN9L58Z3UTktuy9V1f1Y+rGK3t70Truh2wIOJzvZkzobAQ2PwGjjXsr6Q==} hasBin: true + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} @@ -2384,6 +2703,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + unplugin@2.3.10: resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} engines: {node: '>=18.12.0'} @@ -2479,6 +2803,21 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2486,6 +2825,28 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -2494,6 +2855,29 @@ packages: snapshots: + '@atomic-ehr/codegen@0.0.2-canary.20251117105828.24a32f4(typescript@5.9.2)': + dependencies: + '@atomic-ehr/fhir-canonical-manager': 0.0.15(typescript@5.9.2) + '@atomic-ehr/fhirschema': 0.0.5(typescript@5.9.2) + '@inquirer/prompts': 7.10.1 + ajv: 8.17.1 + handlebars: 4.7.8 + ora: 8.2.0 + picocolors: 1.1.1 + yaml: 2.8.1 + yargs: 18.0.0 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@atomic-ehr/fhir-canonical-manager@0.0.15(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@atomic-ehr/fhirschema@0.0.5(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -2935,6 +3319,11 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) + '@health-samurai/aidbox-client@0.0.0-alpha.3': + dependencies: + '@types/json-patch': 0.0.33 + yaml: 2.8.2 + '@health-samurai/react-components@0.0.0-alpha.10(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)': dependencies: '@codemirror/autocomplete': 6.19.0 @@ -3008,6 +3397,103 @@ snapshots: '@standard-schema/utils': 0.3.0 react-hook-form: 7.65.0(react@19.1.1) + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10 + yoctocolors-cjs: 2.1.3 + + '@inquirer/confirm@5.1.21': + dependencies: + '@inquirer/core': 10.3.2 + '@inquirer/type': 3.0.10 + + '@inquirer/core@10.3.2': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/editor@4.2.23': + dependencies: + '@inquirer/core': 10.3.2 + '@inquirer/external-editor': 1.0.3 + '@inquirer/type': 3.0.10 + + '@inquirer/expand@4.0.23': + dependencies: + '@inquirer/core': 10.3.2 + '@inquirer/type': 3.0.10 + yoctocolors-cjs: 2.1.3 + + '@inquirer/external-editor@1.0.3': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.0 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1': + dependencies: + '@inquirer/core': 10.3.2 + '@inquirer/type': 3.0.10 + + '@inquirer/number@3.0.23': + dependencies: + '@inquirer/core': 10.3.2 + '@inquirer/type': 3.0.10 + + '@inquirer/password@4.0.23': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2 + '@inquirer/type': 3.0.10 + + '@inquirer/prompts@7.10.1': + dependencies: + '@inquirer/checkbox': 4.3.2 + '@inquirer/confirm': 5.1.21 + '@inquirer/editor': 4.2.23 + '@inquirer/expand': 4.0.23 + '@inquirer/input': 4.3.1 + '@inquirer/number': 3.0.23 + '@inquirer/password': 4.0.23 + '@inquirer/rawlist': 4.1.11 + '@inquirer/search': 3.2.2 + '@inquirer/select': 4.4.2 + + '@inquirer/rawlist@4.1.11': + dependencies: + '@inquirer/core': 10.3.2 + '@inquirer/type': 3.0.10 + yoctocolors-cjs: 2.1.3 + + '@inquirer/search@3.2.2': + dependencies: + '@inquirer/core': 10.3.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10 + yoctocolors-cjs: 2.1.3 + + '@inquirer/select@4.4.2': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10 + yoctocolors-cjs: 2.1.3 + + '@inquirer/type@3.0.10': {} + '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 @@ -3948,12 +4434,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.13 '@tailwindcss/oxide-win32-x64-msvc': 4.1.13 - '@tailwindcss/vite@4.1.13(vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6))': + '@tailwindcss/vite@4.1.13(vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.2))': dependencies: '@tailwindcss/node': 4.1.13 '@tailwindcss/oxide': 4.1.13 tailwindcss: 4.1.13 - vite: 7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6) + vite: 7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.2) '@tanstack/history@1.132.21': {} @@ -3972,13 +4458,13 @@ snapshots: '@tanstack/query-core': 5.90.2 react: 19.1.1 - '@tanstack/react-router-devtools@1.132.23(@tanstack/react-router@1.132.23(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.132.21)(csstype@3.1.3)(jiti@2.6.0)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.9)(tiny-invariant@1.3.3)(tsx@4.20.6)': + '@tanstack/react-router-devtools@1.132.23(@tanstack/react-router@1.132.23(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(@tanstack/router-core@1.132.21)(csstype@3.1.3)(jiti@2.6.0)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(solid-js@1.9.9)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.2)': dependencies: '@tanstack/react-router': 1.132.23(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@tanstack/router-devtools-core': 1.132.21(@tanstack/router-core@1.132.21)(csstype@3.1.3)(jiti@2.6.0)(lightningcss@1.30.1)(solid-js@1.9.9)(tiny-invariant@1.3.3)(tsx@4.20.6) + '@tanstack/router-devtools-core': 1.132.21(@tanstack/router-core@1.132.21)(csstype@3.1.3)(jiti@2.6.0)(lightningcss@1.30.1)(solid-js@1.9.9)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.2) react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - vite: 7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6) + vite: 7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.2) transitivePeerDependencies: - '@tanstack/router-core' - '@types/node' @@ -4030,14 +4516,14 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.132.21(@tanstack/router-core@1.132.21)(csstype@3.1.3)(jiti@2.6.0)(lightningcss@1.30.1)(solid-js@1.9.9)(tiny-invariant@1.3.3)(tsx@4.20.6)': + '@tanstack/router-devtools-core@1.132.21(@tanstack/router-core@1.132.21)(csstype@3.1.3)(jiti@2.6.0)(lightningcss@1.30.1)(solid-js@1.9.9)(tiny-invariant@1.3.3)(tsx@4.20.6)(yaml@2.8.2)': dependencies: '@tanstack/router-core': 1.132.21 clsx: 2.1.1 goober: 2.1.16(csstype@3.1.3) solid-js: 1.9.9 tiny-invariant: 1.3.3 - vite: 7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6) + vite: 7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.2) optionalDependencies: csstype: 3.1.3 transitivePeerDependencies: @@ -4066,7 +4552,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.132.23(@tanstack/react-router@1.132.23(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6))': + '@tanstack/router-plugin@1.132.23(@tanstack/react-router@1.132.23(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) @@ -4084,7 +4570,7 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.132.23(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - vite: 7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6) + vite: 7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -4166,6 +4652,8 @@ snapshots: '@types/js-yaml@4.0.9': {} + '@types/json-patch@0.0.33': {} + '@types/react-dom@19.2.0(@types/react@19.2.0)': dependencies: '@types/react': 19.2.0 @@ -4176,7 +4664,7 @@ snapshots: '@types/unist@3.0.3': {} - '@vitejs/plugin-react@4.7.0(vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6))': + '@vitejs/plugin-react@4.7.0(vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) @@ -4184,7 +4672,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6) + vite: 7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -4196,6 +4684,23 @@ snapshots: acorn@8.15.0: {} + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + ansis@4.2.0: {} anymatch@3.1.3: @@ -4244,6 +4749,10 @@ snapshots: caniuse-lite@1.0.30001745: {} + chalk@5.6.2: {} + + chardet@2.1.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -4262,6 +4771,20 @@ snapshots: dependencies: clsx: 2.1.1 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cli-width@4.1.0: {} + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + clsx@2.1.1: {} cmdk@1.1.1(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): @@ -4276,6 +4799,12 @@ snapshots: - '@types/react' - '@types/react-dom' + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + commander@2.20.3: {} convert-source-map@2.0.0: {} @@ -4369,6 +4898,10 @@ snapshots: embla-carousel@8.6.0: {} + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 @@ -4409,6 +4942,8 @@ snapshots: eventemitter3@4.0.7: {} + fast-deep-equal@3.1.3: {} + fast-diff@1.3.0: {} fast-equals@5.3.2: {} @@ -4421,6 +4956,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-uri@3.1.0: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -4438,6 +4975,10 @@ snapshots: gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + + get-east-asian-width@1.4.0: {} + get-nonce@1.0.1: {} get-tsconfig@4.10.1: @@ -4454,8 +4995,21 @@ snapshots: graceful-fs@4.2.11: {} + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + highlight.js@11.11.1: {} + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + input-otp@1.4.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: react: 19.1.1 @@ -4469,12 +5023,20 @@ snapshots: is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-interactive@2.0.0: {} + is-number@7.0.0: {} + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + isbot@5.1.31: {} jiti@2.6.0: {} @@ -4489,6 +5051,8 @@ snapshots: jsesc@3.1.0: {} + json-schema-traverse@1.0.0: {} + json5@2.2.3: {} lightningcss-darwin-arm64@1.30.1: @@ -4538,6 +5102,11 @@ snapshots: lodash@4.17.21: {} + log-symbols@6.0.0: + dependencies: + chalk: 5.6.2 + is-unicode-supported: 1.3.0 + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -4567,6 +5136,10 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mimic-function@5.0.1: {} + + minimist@1.2.8: {} + minipass@7.1.2: {} minizlib@3.1.0: @@ -4577,6 +5150,8 @@ snapshots: ms@2.1.3: {} + mute-stream@2.0.0: {} + nanoid@3.3.11: {} nearley@2.20.1: @@ -4586,6 +5161,8 @@ snapshots: railroad-diagrams: 1.0.0 randexp: 0.4.6 + neo-async@2.6.2: {} + next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: react: 19.1.1 @@ -4597,6 +5174,22 @@ snapshots: object-assign@4.1.1: {} + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + ora@8.2.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.2 + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -4800,8 +5393,15 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + ret@0.1.15: {} reusify@1.1.0: {} @@ -4838,6 +5438,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + safer-buffer@2.1.2: {} + scheduler@0.26.0: {} semver@6.3.1: {} @@ -4848,6 +5450,8 @@ snapshots: seroval@1.3.2: {} + signal-exit@4.1.0: {} + solid-js@1.9.9: dependencies: csstype: 3.1.3 @@ -4870,6 +5474,28 @@ snapshots: argparse: 2.0.1 nearley: 2.20.1 + stdin-discarder@0.2.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + style-mod@4.1.3: {} tailwind-merge@3.3.1: {} @@ -4912,6 +5538,9 @@ snapshots: typescript@5.9.2: {} + uglify-js@3.19.3: + optional: true + unplugin@2.3.10: dependencies: '@jridgewell/remapping': 2.3.5 @@ -4974,7 +5603,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6): + vite@7.1.7(jiti@2.6.0)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.2): dependencies: esbuild: 0.25.10 fdir: 6.5.0(picomatch@4.0.3) @@ -4987,15 +5616,49 @@ snapshots: jiti: 2.6.0 lightningcss: 1.30.1 tsx: 4.20.6 + yaml: 2.8.2 w3c-keyname@2.2.8: {} webpack-virtual-modules@0.6.2: {} + wordwrap@1.0.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + + y18n@5.0.8: {} + yallist@3.1.1: {} yallist@5.0.0: {} + yaml@2.8.1: {} + + yaml@2.8.2: {} + + yargs-parser@22.0.0: {} + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yoctocolors-cjs@2.1.3: {} + zod@3.25.76: {} zod@4.1.12: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index 5b23a1c..0000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,3 +0,0 @@ -onlyBuiltDependencies: - - '@tailwindcss/oxide' - - esbuild diff --git a/scripts/generate-types.ts b/scripts/generate-types.ts new file mode 100644 index 0000000..733424c --- /dev/null +++ b/scripts/generate-types.ts @@ -0,0 +1,37 @@ +import { APIBuilder } from "@atomic-ehr/codegen"; + +console.log("📦 Generating FHIR R4 Core Types..."); + +const builder = new APIBuilder() + .verbose() + .throwException() + .typescript({ + withDebugComment: false, + generateProfile: false, + openResourceTypeSet: true, + }) + .fromPackageRef("https://build.fhir.org/ig/FHIR/sql-on-fhir-v2/package.tgz") + .outputTo("./src/fhir-types") + // .writeTypeTree("./src/fhir-types/tree.yaml") + .treeShake({ + "hl7.fhir.r5.core": { + "http://hl7.org/fhir/StructureDefinition/OperationOutcome": {}, + "http://hl7.org/fhir/StructureDefinition/Bundle": {}, + "http://hl7.org/fhir/StructureDefinition/Resource": {}, + }, + "org.sql-on-fhir.ig": { + "https://sql-on-fhir.org/ig/StructureDefinition/ViewDefinition": {}, + }, + }) + .cleanOutput(true); + +const report = await builder.generate(); + +console.log(report); + +if (report.success) { + console.log("✅ FHIR types generated successfully!"); +} else { + console.error("❌ FHIR types generation failed."); + process.exit(1); +} diff --git a/src/AidboxClient.tsx b/src/AidboxClient.tsx new file mode 100644 index 0000000..90837b9 --- /dev/null +++ b/src/AidboxClient.tsx @@ -0,0 +1,58 @@ +import type * as Aidbox from "@health-samurai/aidbox-client"; +import { + AidboxClient, + BrowserAuthProvider, +} from "@health-samurai/aidbox-client"; +import * as React from "react"; +import type { + Bundle, + OperationOutcome, + Resource, +} from "./fhir-types/hl7-fhir-r5-core"; + +// FIXME: sansara#6557 Generate from IG +export type User = Resource & { + email?: string; +}; + +export type AidboxClientR5 = Aidbox.AidboxClient< + Bundle, + OperationOutcome, + User +>; + +export const AidboxClientContext = React.createContext< + AidboxClientR5 | undefined +>(undefined); + +export type AidboxClientProviderProps = { + baseurl: string; + children: React.ReactNode; +}; + +export function AidboxClientProvider({ + baseurl, + children, +}: AidboxClientProviderProps): React.JSX.Element { + const client = new AidboxClient( + baseurl, + new BrowserAuthProvider(baseurl), + ); + + return ( + + {children} + + ); +} + +export function useAidboxClient(aidboxClient?: AidboxClientR5): AidboxClientR5 { + const client = React.useContext(AidboxClientContext); + + if (aidboxClient) return aidboxClient; + + if (!client) + throw new Error("No AidboxClient set, use AidboxClientProvider to set one"); + + return client; +} diff --git a/src/api/auth.ts b/src/api/auth.ts index dae44c2..de657aa 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,319 +1,57 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import Cookies from "js-cookie"; -import type { UIHistoryResponse } from "../shared/types"; -import { getAidboxBaseURL } from "../utils"; - -export interface UserInfo { - id: string; - email?: string; -} - -export interface AidboxRequestParams { - method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; - url: string; - headers?: Record; - params?: [string, string][]; - body?: string; - streamBody?: boolean; -} - -export interface AidboxResponse { - response: { - status: number; - statusText: string; - headers: Record; - body: string | ReadableStream | null; - }; - meta: { - duration: number; - request: { - method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; - url: string; - headers?: Record; - params?: [string, string][]; - body?: string; - }; - }; -} - -const defaultHeaders = { - "Content-Type": "application/json", - Accept: "application/json", -}; - -// A modified copy of AidboxCallWithMeta but with fixed error -// reporting, and unified types for successful response and error -// response. -export async function AidboxRequest({ - method, - url, - headers = {}, - params = [], - body, - streamBody = false, -}: AidboxRequestParams): Promise { - const startTime = Date.now(); - const baseURL = getAidboxBaseURL(); - - const urlObj = new URL(url.startsWith("/") ? url.slice(1) : url, baseURL); - params.forEach(([key, value]) => { - urlObj.searchParams.append(key, value); - }); - - const requestHeaders = { ...defaultHeaders, ...headers }; - - const response = await fetch(urlObj.toString(), { - method, - headers: requestHeaders, - body: body || null, - credentials: "include", - }); - const responseHeaders: Record = {}; - response.headers.forEach((value, key) => { - responseHeaders[key] = value; - }); - - const result: AidboxResponse = { - response: { - status: response.status, - statusText: response.statusText, - headers: responseHeaders, - body: streamBody ? response.body : await response.text(), - }, - meta: { - duration: Date.now() - startTime, - request: { - method, - url, - params, - headers: requestHeaders, - body: body || "", - }, - }, - }; - - if (!response.ok) { - if (response.status === 401 || response.status === 403) { - const encodedLocation = btoa(window.location.href); - window.location.href = `${baseURL}/auth/login?redirect_to=${encodedLocation}`; - throw Error("Authentication required", { cause: result }); - } - - throw Error(`HTTP ${response.status}: ${response.statusText}`, { - cause: result, - }); - } - - return result; -} - -export interface AidboxCallParams { - method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; - url: string; - headers?: Record; - params?: Record; - body?: string | object; -} - -// TODO: ditch AidboxCall and AidboxCallWithMeta in favor of -// AidboxRequest across the project. - -// assumes JSON too much -export async function AidboxCall({ - method, - url, - headers = {}, - params = {}, - body, -}: AidboxCallParams): Promise { - const baseURL = getAidboxBaseURL(); - - const urlObj = new URL(url.startsWith("/") ? url.slice(1) : url, baseURL); - Object.entries(params).forEach(([key, value]) => { - urlObj.searchParams.append(key, value); - }); - - const requestHeaders = { ...defaultHeaders, ...headers }; - - let requestBody: string | null = null; - if (body) { - if (typeof body === "string") { - requestBody = body; - } else { - requestBody = JSON.stringify(body); - } - } - - const response = await fetch(urlObj.toString(), { - method, - headers: requestHeaders, - body: requestBody, - credentials: "include", - }); - - if (!response.ok) { - if (response.status === 401 || response.status === 403) { - const encodedLocation = btoa(window.location.href); - window.location.href = `${baseURL}/auth/login?redirect_to=${encodedLocation}`; - throw new Error("Authentication required"); - } - throw Error(`HTTP ${response.status}: ${response.statusText}`, { - cause: await response.json(), - }); - } - - const contentType = response.headers.get("content-type"); - if (!contentType || !contentType.includes("application/json")) { - return null as T; - } - - return response.json() as T; -} - -// unusable errors: have to guess error content type. Hard to -// refactor, as error type is unspecified by TS -export async function AidboxCallWithMeta({ - method, - url, - headers = {}, - params = {}, - body, -}: AidboxCallParams): Promise<{ - status: number; - statusText: string; - headers: Record; - body: string; - duration: number; -}> { - const startTime = Date.now(); - const baseURL = getAidboxBaseURL(); - - const urlObj = new URL(url.startsWith("/") ? url.slice(1) : url, baseURL); - Object.entries(params).forEach(([key, value]) => { - urlObj.searchParams.append(key, value); - }); - - const requestHeaders = { ...defaultHeaders, ...headers }; - - let requestBody: string | null = null; - if (body) { - if (typeof body === "string") { - requestBody = body; - } else { - requestBody = JSON.stringify(body); - } - } - - const response = await fetch(urlObj.toString(), { - method, - headers: requestHeaders, - body: requestBody, - credentials: "include", - }); - - const duration = Date.now() - startTime; - - const responseHeaders: Record = {}; - response.headers.forEach((value, key) => { - responseHeaders[key] = value; - }); - - const bodyText = await response.text(); - - if (!response.ok) { - if (response.status === 401 || response.status === 403) { - const encodedLocation = btoa(window.location.href); - window.location.href = `${baseURL}/auth/login?redirect_to=${encodedLocation}`; - throw new Error("Authentication required"); - } - throw Error(`HTTP ${response.status}: ${response.statusText}`, { - cause: bodyText, - }); - } - - return { - status: response.status, - statusText: response.statusText, - headers: responseHeaders, - body: bodyText, - duration, - }; -} - -async function fetchUserInfo(): Promise { - const response = await fetch(`${getAidboxBaseURL()}/auth/userinfo`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "include", - }); - - if (!response.ok) { - const encodedLocation = btoa(window.location.href); - window.location.href = `${getAidboxBaseURL()}/auth/login?redirect_to=${encodedLocation}`; - } - - return response.json(); -} +import { redirect } from "@tanstack/react-router"; +import { useAidboxClient } from "../AidboxClient"; export function useUserInfo() { + const client = useAidboxClient(); + return useQuery({ queryKey: ["userInfo"], - queryFn: fetchUserInfo, + queryFn: client.userinfo, refetchOnWindowFocus: false, }); } -async function performLogout() { - const response = await fetch(`${getAidboxBaseURL()}/auth/logout`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "include", - }); - - Cookies.remove("asid", { path: "/" }); - - const encodedLocation = btoa(window.location.href); - window.location.href = `${getAidboxBaseURL()}/auth/login?redirect_to=${encodedLocation}`; - - return response; -} - export function useLogout() { + const client = useAidboxClient(); const queryClient = useQueryClient(); return useMutation({ - mutationFn: performLogout, + mutationFn: client.logout, onSuccess: () => { queryClient.removeQueries({ queryKey: ["userInfo"] }); + const encodedLocation = btoa(window.location.href); + const redirectTo = `${client.getBaseUrl()}/auth/login?redirect_to=${encodedLocation}`; + window.location.href = redirectTo; + // FIXME: doesn't work without window.location.href + throw redirect({ href: redirectTo }); }, }); } -// UI History API -async function fetchUIHistory(): Promise { - const response = await AidboxCall({ - method: "GET", - url: "/ui_history", - params: { - ".type": "http", - _sort: "-_lastUpdated", - _count: "100", - }, - }); - - return response; -} - export function useUIHistory() { + const client = useAidboxClient(); + return useQuery({ queryKey: ["uiHistory"], - queryFn: fetchUIHistory, + queryFn: async () => { + const result = await client.searchType({ + type: "ui_history", + query: [ + [".type", "http"], + ["_sort", "-_lastUpdated"], + ["_count", "100"], + ], + }); + + if (result.isOk()) { + const { resource: history } = result.value; + return history; + } else { + const { resource: oo } = result.value; + throw new Error("error fetching history", { cause: oo }); + } + }, refetchOnWindowFocus: false, staleTime: 30000, // 30 seconds }); diff --git a/src/api/utils.tsx b/src/api/utils.tsx index 4067411..6e30cdb 100644 --- a/src/api/utils.tsx +++ b/src/api/utils.tsx @@ -1,80 +1,112 @@ -import { hasProperty, isArray } from "@aidbox-ui/type-utils"; +import type { OperationOutcome } from "@aidbox-ui/fhir-types/hl7-fhir-r5-core"; +import { isOperationOutcome } from "@aidbox-ui/fhir-types/hl7-fhir-r5-core"; +import { hasProperty } from "@aidbox-ui/type-utils"; +import type * as AidboxTypes from "@health-samurai/aidbox-client"; +import { ErrorResponse } from "@health-samurai/aidbox-client"; import * as HSComp from "@health-samurai/react-components"; import type { MutationFunctionContext } from "@tanstack/react-query"; +import type { JSX } from "react"; -export function onError( - cb?: ( - error: Error, - vars: T, - onMutateResult: unknown, - context: MutationFunctionContext, - ) => Promise | unknown, -) { - return ( - error: Error, - vars: T, - onMutateResult: unknown, - context: MutationFunctionContext, - ) => { - if (error.cause !== null) { - const cause: unknown = - typeof error.cause === "string" ? JSON.parse(error.cause) : error.cause; - - if ( - typeof cause !== "object" || - cause === null || - !hasProperty(cause, "issue") || - !isArray(cause.issue) - ) { - HSComp.toast.error("Unknown error", { - position: "bottom-right", - style: { - margin: "1rem", - backgroundColor: "var(--destructive)", - color: "var(--accent)", - }, - }); - return; - } +export function toastError(expression: string, diagnostics?: string) { + let message: JSX.Element; + if (diagnostics) { + message = ( +
+ {expression} +

{diagnostics}

+
+ ); + } else { + message = ( +
+ {expression} +
+ ); + } + HSComp.toast.error(message, { + position: "bottom-right", + style: { + margin: "1rem", + backgroundColor: "var(--destructive)", + color: "var(--accent)", + }, + }); +} - const issues = cause.issue; - issues.forEach((o) => { - if (typeof o !== "object" || o === null) { - console.error("Invalid OperationOutcome error"); - return; - } +export function parseOperationOutcome( + oo: OperationOutcome, +): { expression: string; diagnostics: string }[] { + const issues = oo.issue; - const expression = - hasProperty(o, "expression") && typeof o.expression === "string" - ? o.expression - : null; + return issues.flatMap((issue) => { + if (typeof issue !== "object" || issue === null) { + return []; + } - const diagnostics = - hasProperty(o, "diagnostics") && typeof o.diagnostics === "string" - ? o.diagnostics - : null; + const expression = + hasProperty(issue, "expression") && typeof issue.expression === "string" + ? issue.expression + : null; - if (expression === null && diagnostics === null) { - console.error("Empty OperationOutcome error"); - return; - } + const diagnostics = + hasProperty(issue, "diagnostics") && typeof issue.diagnostics === "string" + ? issue.diagnostics + : null; - HSComp.toast.error( -
- {expression} -

{diagnostics}

-
, - { - position: "bottom-right", - style: { - margin: "1rem", - backgroundColor: "var(--destructive)", - color: "var(--accent)", - }, - }, - ); - }); + if (expression === null && diagnostics === null) { + return []; } - return cb?.(error, vars, onMutateResult, context); - }; + + return { + expression: expression || "Error", + diagnostics: diagnostics || "unknown error", + }; + }); +} + +export function toastOperationOutcome(oo: OperationOutcome) { + const issues = parseOperationOutcome(oo); + if (issues.length === 0) + return toastError( + "Invalid OperationOutcome: no details provided", + JSON.stringify(oo), + ); + + issues.forEach(({ expression, diagnostics }) => { + toastError(expression, diagnostics); + }); +} + +export async function toastErrorResponse(error: Error) { + if (error instanceof ErrorResponse) { + const reason: AidboxTypes.ResponseWithMeta = error.responseWithMeta; + const body = await reason.response.text(); + try { + const parsed = JSON.parse(body); + if (isOperationOutcome(parsed)) return toastOperationOutcome(parsed); + } catch {} + } + return toastError("Error", error.message); +} + +export async function onError(error: unknown) { + if (error instanceof Error) { + if (error instanceof ErrorResponse) return await toastErrorResponse(error); + + if (isOperationOutcome(error.cause)) + return toastOperationOutcome(error.cause); + + return toastError("Error", error.message); + } else { + return toastError("Error", "unknown error"); + } +} + +export async function onMutationError( + error: Error, + _vars: T, + _onMutateResult: unknown, + _context: MutationFunctionContext, +) { + return onError(error); } diff --git a/src/components/ResourceBrowser/browser.tsx b/src/components/ResourceBrowser/browser.tsx index ad74eb6..f1b8e41 100644 --- a/src/components/ResourceBrowser/browser.tsx +++ b/src/components/ResourceBrowser/browser.tsx @@ -1,10 +1,10 @@ -import { AidboxCallWithMeta } from "@aidbox-ui/api/auth"; import { useLocalStorage } from "@aidbox-ui/hooks/useLocalStorage"; import * as HSComp from "@health-samurai/react-components"; import { useQuery } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; import { Pin } from "lucide-react"; import { memo, useMemo, useState } from "react"; +import { type AidboxClientR5, useAidboxClient } from "../../AidboxClient"; type ResourceRow = { resourceType: string; @@ -182,23 +182,23 @@ type ResourceData = { stats: Record; }; -function useResourceData() { +function useResourceData(client: AidboxClientR5) { return useQuery({ queryKey: ["resource-browser-resources"], queryFn: async () => { const [resourceTypes, stats] = await Promise.all([ - AidboxCallWithMeta({ + client.rawRequest({ method: "GET", url: "/$resource-types", }), - AidboxCallWithMeta({ + client.rawRequest({ method: "GET", url: "/$resource-types-pg-stats", }), ]); return { - resources: JSON.parse(resourceTypes.body), - stats: JSON.parse(stats.body), + resources: await resourceTypes.response.json(), + stats: await stats.response.json(), }; }, }); @@ -261,6 +261,8 @@ function useProcessedData( } export function Browser() { + const client = useAidboxClient(); + const [selectedTab, setSelectedTab] = useLocalStorage({ key: "resource-browser-selected-tab", defaultValue: "all", @@ -280,7 +282,7 @@ export function Browser() { [favoritesRef.current], ); - const { data, isLoading } = useResourceData(); + const { data, isLoading } = useResourceData(client); const { subsets } = useProcessedData(data, favorites); const toggleFavorite = useMemo( diff --git a/src/components/ResourceBrowser/page.tsx b/src/components/ResourceBrowser/page.tsx index 56b4e2f..6ae656c 100644 --- a/src/components/ResourceBrowser/page.tsx +++ b/src/components/ResourceBrowser/page.tsx @@ -1,10 +1,10 @@ +import type { Resource } from "@aidbox-ui/fhir-types/hl7-fhir-r5-core"; import * as HSComp from "@health-samurai/react-components"; import * as ReactQuery from "@tanstack/react-query"; import * as Router from "@tanstack/react-router"; import * as Lucide from "lucide-react"; import * as React from "react"; -import * as AidboxClient from "../../api/auth"; -import { AidboxCallWithMeta } from "../../api/auth"; +import type { AidboxClientR5 } from "../../AidboxClient"; import * as Humanize from "../../humanize"; import * as Utils from "../../utils"; import type * as VDTypes from "../ViewDefinition/types"; @@ -131,9 +131,10 @@ export const ResourcesTabHeader = ({ }; const fetchSchemas = async ( + client: AidboxClientR5, resourceType: string, ): Promise | undefined> => { - const response = await AidboxCallWithMeta({ + const response = await client.rawRequest({ method: "POST", url: "/rpc?_m=aidbox.introspector/get-schemas-by-resource-type", headers: { @@ -145,7 +146,7 @@ const fetchSchemas = async ( }), }); - const data: SchemaData = JSON.parse(response.body); + const data: SchemaData = await response.response.json(); if (!data?.result) return undefined; @@ -153,9 +154,10 @@ const fetchSchemas = async ( }; const fetchDefaultSchema = async ( + client: AidboxClientR5, resourceType: string, ): Promise => { - const schemas = await fetchSchemas(resourceType); + const schemas = await fetchSchemas(client, resourceType); if (!schemas) return undefined; @@ -168,10 +170,10 @@ const fetchDefaultSchema = async ( const resourcesWithKeys = ( profiles: Schema | undefined, - resources: Array>, + resources: Resource[], ) => { const resourceKeys: Record = resources.reduce( - (acc: Record, resource: Record) => { + (acc: Record, resource: Resource) => { Object.keys(resource).forEach((key) => { acc[key] = undefined; }); @@ -211,7 +213,7 @@ export const ResourcesTabTable = ({ data }: Types.ResourcesTabTableProps) => { const { resources, resourceKeys, snapshot } = data; - const columns: HSComp.ColumnDef[] = [ + const columns: HSComp.ColumnDef[] = [ { accessorKey: "id", header: () => ID, @@ -258,11 +260,10 @@ export const ResourcesTabTable = ({ data }: Types.ResourcesTabTableProps) => { ); }; -type FhirBundle = { - entry: { resource: T }[]; -}; - -const ResourcesTabContent = ({ resourceType }: Types.ResourcesPageProps) => { +const ResourcesTabContent = ({ + client, + resourceType, +}: Types.ResourcesPageProps) => { const resourcesPageContext = React.useContext(ResourcesPageContext); const navigate = Router.useNavigate(); @@ -271,27 +272,42 @@ const ResourcesTabContent = ({ resourceType }: Types.ResourcesPageProps) => { }); const decodedSearchQuery = search.searchQuery - ? atob(search.searchQuery) + ? search.searchQuery : Constants.DEFAULT_SEARCH_QUERY; - const { data, isLoading } = ReactQuery.useQuery({ + const { data, isLoading, error } = ReactQuery.useQuery({ queryKey: [Constants.PageID, "resource-list", decodedSearchQuery], queryFn: async () => { - const response = await AidboxClient.AidboxCallWithMeta({ - method: "GET", - url: `/fhir/${resourcesPageContext.resourceType}?${decodedSearchQuery}`, + const result = await client.searchType({ + type: resourcesPageContext.resourceType, + query: Utils.formatSearchQuery(decodedSearchQuery), }); - const bundle: FhirBundle> = JSON.parse( - response.body, - ); + if (result.isErr()) + throw new Error("error obtaining resource list", { + cause: result.value, + }); + + const { resource: bundle } = result.value; - const data = bundle.entry.map((entry) => entry.resource); - const schema = await fetchDefaultSchema(resourceType); + const data = + bundle?.entry?.flatMap(({ resource }) => (resource ? resource : [])) ?? + []; + const schema = await fetchDefaultSchema(client, resourceType); return resourcesWithKeys(schema, data); }, retry: false, }); + if (error) + return ( +
+
+
Failed to load resource
+
{error.message}
+
+
+ ); + const handleSearch = (e: React.FormEvent) => { e.preventDefault(); navigate({ @@ -312,7 +328,10 @@ const ResourcesTabContent = ({ resourceType }: Types.ResourcesPageProps) => { ); }; -const ProfilesTabContent = ({ resourceType }: Types.ResourcesPageProps) => { +const ProfilesTabContent = ({ + client, + resourceType, +}: Types.ResourcesPageProps) => { const [selectedProfile, setSelectedProfile] = React.useState( null, ); @@ -321,7 +340,7 @@ const ProfilesTabContent = ({ resourceType }: Types.ResourcesPageProps) => { const { data, isLoading } = ReactQuery.useQuery({ queryKey: [Constants.PageID, "resource-profiles-list"], queryFn: async () => { - const schema = await fetchSchemas(resourceType); + const schema = await fetchSchemas(client, resourceType); return schema; }, retry: false, @@ -519,12 +538,13 @@ type PartialFhirCapabilityStatement = { }; const SearchParametersTabContent = ({ + client, resourceType, }: Types.ResourcesPageProps) => { const { data, isLoading } = ReactQuery.useQuery({ queryKey: [Constants.PageID, "resource-search-parameters-list"], queryFn: async () => { - const response = await AidboxCallWithMeta({ + const response = await client.rawRequest({ method: "GET", url: "/fhir/metadata?include-custom-resources=true", headers: { @@ -532,7 +552,7 @@ const SearchParametersTabContent = ({ }, }); - const data = JSON.parse(response.body); + const data = await response.response.json(); // FIXME: validate return data as PartialFhirCapabilityStatement; }, @@ -606,19 +626,25 @@ const SearchParametersTabContent = ({ ); }; -export const ResourcesPage = ({ resourceType }: Types.ResourcesPageProps) => { +export const ResourcesPage = ({ + client, + resourceType, +}: Types.ResourcesPageProps) => { return ( - + - + - + diff --git a/src/components/ResourceBrowser/types.tsx b/src/components/ResourceBrowser/types.tsx index 2a9c76b..0550e59 100644 --- a/src/components/ResourceBrowser/types.tsx +++ b/src/components/ResourceBrowser/types.tsx @@ -1,16 +1,12 @@ +import type { Resource } from "@aidbox-ui/fhir-types/hl7-fhir-r5-core"; import type { Snapshot } from "@aidbox-ui/humanize"; +import type { AidboxClientR5 } from "../../AidboxClient"; export interface ResourcesPageProps { + client: AidboxClientR5; resourceType: string; } -export type Resource = { - id?: string; - meta?: { - lastUpdated: string; - }; -}; - export interface ResourcesTabTableProps { data: | { diff --git a/src/components/ResourceEditor/action.tsx b/src/components/ResourceEditor/action.tsx index ba1f048..7685a0b 100644 --- a/src/components/ResourceEditor/action.tsx +++ b/src/components/ResourceEditor/action.tsx @@ -1,15 +1,12 @@ import { defaultToastPlacement } from "@aidbox-ui/components/config"; +import type { Resource } from "@aidbox-ui/fhir-types/hl7-fhir-r5-core"; import * as HSComp from "@health-samurai/react-components"; import { useMutation } from "@tanstack/react-query"; import * as Router from "@tanstack/react-router"; import * as YAML from "js-yaml"; -import { - createResource, - deleteResource, - type Resource, - updateResource, -} from "./api"; +import type { AidboxClientR5 } from "../../AidboxClient"; import * as Utils from "../../api/utils"; +import { createResource, deleteResource, updateResource } from "./api"; import type { EditorMode } from "./types"; export const SaveButton = ({ @@ -17,11 +14,13 @@ export const SaveButton = ({ id, resource, mode, + client, }: { resourceType: string; id: string | undefined; resource: string; mode: EditorMode; + client: AidboxClientR5; }) => { const navigate = Router.useNavigate(); const mutation = useMutation({ @@ -29,13 +28,17 @@ export const SaveButton = ({ const resource = ( mode === "json" ? JSON.parse(value) : YAML.load(value) ) as Resource; - if (id) return await updateResource(resourceType, id, resource); - return await createResource(resourceType, resource); + if (id) return await updateResource(client, resourceType, id, resource); + return await createResource(client, resourceType, resource); }, - onError: Utils.onError(), + onError: Utils.onMutationError, onSuccess: (resource, _variables, _onMutateResult, _context) => { HSComp.toast.success("Saved", defaultToastPlacement); - if (!resource.id) throw new Error("Resource ID is undefined"); + if (!resource.id) + return Utils.toastError( + "Failed to open saved resource", + "resource is missing an ID field", + ); if (!id) navigate({ to: `/resource/$resourceType/edit/$id`, @@ -61,16 +64,18 @@ export const SaveButton = ({ export const DeleteButton = ({ resourceType, id, + client, }: { resourceType: string; id: string; + client: AidboxClientR5; }) => { const navigate = Router.useNavigate(); const mutation = useMutation({ mutationFn: async () => { - return await deleteResource(resourceType, id); + return await deleteResource(client, resourceType, id); }, - onError: Utils.onError(), + onError: Utils.onMutationError, onSuccess: (_resource, _variables, _onMutateResult, _context) => { HSComp.toast.success("Saved", defaultToastPlacement); navigate({ diff --git a/src/components/ResourceEditor/api.ts b/src/components/ResourceEditor/api.ts index b6b4141..da17455 100644 --- a/src/components/ResourceEditor/api.ts +++ b/src/components/ResourceEditor/api.ts @@ -1,101 +1,110 @@ -import { AidboxCall } from "@aidbox-ui/api/auth"; +import { parseOperationOutcome } from "@aidbox-ui/api/utils"; +import type { Bundle, Resource } from "@aidbox-ui/fhir-types/hl7-fhir-r5-core"; +import type { AidboxClientR5 } from "../../AidboxClient"; -export type Resource = { - resourceType: string; - id?: string; - [key: string]: unknown; -}; - -export const fetchResource = async (resourceType: string, id: string) => { - const raw = await AidboxCall({ - method: "GET", - url: `/fhir/${resourceType}/${id}`, - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, +export const fetchResource = async ( + client: AidboxClientR5, + resourceType: string, + id: string, +): Promise => { + const result = await client.read({ + type: resourceType, + id: id, }); - return raw; -}; -export interface HistoryBundle { - resourceType: "Bundle"; - type: "history"; - total: number; - entry: HistoryEntry[]; -} + if (result.isErr()) + throw new Error( + parseOperationOutcome(result.value.resource) + .map(({ expression, diagnostics }) => `${expression}: ${diagnostics}`) + .join("; "), + { cause: result.value.resource }, + ); -export type HistoryEntryResource = { - meta: { - versionId: string; - lastUpdated: string; - }; - resourceType: string; - id: string; + return result.value.resource; }; -export interface HistoryEntry { - resource: HistoryEntryResource; - response: { status: string }; -} - export const fetchResourceHistory = async ( + client: AidboxClientR5, resourceType: string, id: string, -) => { - const raw = await AidboxCall({ - method: "GET", - url: `/fhir/${resourceType}/${id}/_history`, - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - params: { _page: "1", _count: "100" }, +): Promise => { + const result = await client.historyInstance({ + type: resourceType, + id: id, }); - return raw as HistoryBundle; + + if (result.isErr()) + throw new Error( + parseOperationOutcome(result.value.resource) + .map(({ expression, diagnostics }) => `${expression}: ${diagnostics}`) + .join("; "), + { cause: result.value.resource }, + ); + + return result.value.resource; }; export const createResource = async ( + client: AidboxClientR5, resourceType: string, resource: Resource, ) => { - const res = await AidboxCall({ - method: "POST", - url: `/fhir/${resourceType}`, - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: resource, + const result = await client.create({ + type: resourceType, + resource: resource, }); - return res; + + if (result.isErr()) + throw new Error( + parseOperationOutcome(result.value.resource) + .map(({ expression, diagnostics }) => `${expression}: ${diagnostics}`) + .join("; "), + { cause: result.value.resource }, + ); + + return result.value.resource; }; export const updateResource = async ( + client: AidboxClientR5, resourceType: string, id: string, resource: Resource, ) => { - const res = await AidboxCall({ - method: "PUT", - url: `/fhir/${resourceType}/${id}`, - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: resource, + const result = await client.update({ + type: resourceType, + id: id, + resource: resource, }); - return res; + + if (result.isErr()) + throw new Error( + parseOperationOutcome(result.value.resource) + .map(({ expression, diagnostics }) => `${expression}: ${diagnostics}`) + .join("; "), + { cause: result.value }, + ); + + return result.value.resource; }; -export const deleteResource = async (resourceType: string, id: string) => { - const res = await AidboxCall({ - method: "DELETE", - url: `/fhir/${resourceType}/${id}`, - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, +export const deleteResource = async ( + client: AidboxClientR5, + resourceType: string, + id: string, +) => { + const result = await client.delete({ + type: resourceType, + id: id, }); - return res; + + if (result.isErr()) + throw new Error( + parseOperationOutcome(result.value.resource) + .map(({ expression, diagnostics }) => `${expression}: ${diagnostics}`) + .join("; "), + { cause: result.value.resource }, + ); + + return result.value.resource; }; diff --git a/src/components/ResourceEditor/page.tsx b/src/components/ResourceEditor/page.tsx index 4f28f1f..a0193f2 100644 --- a/src/components/ResourceEditor/page.tsx +++ b/src/components/ResourceEditor/page.tsx @@ -1,10 +1,12 @@ +import type { Resource } from "@aidbox-ui/fhir-types/hl7-fhir-r5-core"; import * as HSComp from "@health-samurai/react-components"; import { useQuery } from "@tanstack/react-query"; import type * as Router from "@tanstack/react-router"; import * as YAML from "js-yaml"; import React from "react"; +import { useAidboxClient } from "../../AidboxClient"; import { DeleteButton, SaveButton } from "./action"; -import { fetchResource, type Resource } from "./api"; +import { fetchResource } from "./api"; import { EditorTab } from "./editor-tab"; import { type EditorMode, pageId, type ResourceEditorTab } from "./types"; import { VersionsTab } from "./versions-tab"; @@ -23,6 +25,8 @@ interface ResourceEditorPageProps { export const ResourceEditorPageWithLoader = ( props: ResourceEditorPageProps, ) => { + const client = useAidboxClient(); + const { resourceType, id } = props; const { @@ -34,7 +38,7 @@ export const ResourceEditorPageWithLoader = ( queryKey: [pageId, resourceType, id], queryFn: async () => { if (!id) throw new Error("Impossible"); - return await fetchResource(resourceType, id); + return await fetchResource(client, resourceType, id); }, retry: false, }); @@ -60,7 +64,17 @@ export const ResourceEditorPageWithLoader = ( ); } - if (!resourceData) throw new Error("Resource not found"); + + if (!resourceData) + return ( +
+
+
Failed to load resource
+
Resource not found
+
+
+ ); + return ; }; @@ -73,6 +87,8 @@ export const ResourceEditorPage = ({ navigate, initialResource, }: ResourceEditorPageProps & { initialResource: Resource }) => { + const client = useAidboxClient(); + const [resource, setResource] = React.useState(initialResource); const [resourceText, setResourceText] = React.useState(() => { if (mode === "yaml") { @@ -142,6 +158,7 @@ export const ResourceEditorPage = ({ id={id} resource={resourceText} mode={mode} + client={client} /> ), }, @@ -159,7 +176,9 @@ export const ResourceEditorPage = ({ ), }); actions.push({ - content: , + content: ( + + ), }); } diff --git a/src/components/ResourceEditor/versions-tab.tsx b/src/components/ResourceEditor/versions-tab.tsx index d171bd7..5cff024 100644 --- a/src/components/ResourceEditor/versions-tab.tsx +++ b/src/components/ResourceEditor/versions-tab.tsx @@ -1,18 +1,18 @@ -import { AidboxCallWithMeta } from "@aidbox-ui/api/auth"; +import type { + Bundle, + BundleEntry, + Resource, +} from "@aidbox-ui/fhir-types/hl7-fhir-r5-core"; import { DiffView } from "@git-diff-view/react"; import * as HSComp from "@health-samurai/react-components"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import * as YAML from "js-yaml"; import React from "react"; -import * as utils from "../../api/utils"; +import { useAidboxClient } from "../../AidboxClient"; +import * as Utils from "../../api/utils"; import { diff } from "../../utils/diff"; import { traverseTree } from "../../utils/tree-walker"; -import { - fetchResourceHistory, - type HistoryBundle, - type HistoryEntry, - type HistoryEntryResource, -} from "./api"; +import { fetchResourceHistory } from "./api"; import { type EditorMode, pageId } from "./types"; import "@git-diff-view/react/styles/diff-view-pure.css"; import { generateDiffFile } from "@git-diff-view/file"; @@ -26,8 +26,8 @@ type historyWithHistoryProps = { versionId: string; date: Date | string; status: string; - resourceCurrent: HistoryEntryResource; - resourcePrevious: HistoryEntryResource | null; + resourceCurrent: Resource; + resourcePrevious: Resource | null; affected: Set<(string | number)[]>; }; @@ -78,13 +78,15 @@ const VersionDiffDialog = ({ openState, onOpenChange, }: { - previous: HistoryEntryResource | null; - current: HistoryEntryResource; + previous: Resource | null; + current: Resource; resourceType: string; resourceId: string; openState: OpenState; onOpenChange: (open: OpenState) => void; }) => { + const client = useAidboxClient(); + const diff = generateDiffFile( "prev.json", JSON.stringify(previous, null, " "), @@ -101,14 +103,10 @@ const VersionDiffDialog = ({ const mutation = useMutation({ mutationFn: (resource: string) => { - return AidboxCallWithMeta({ - method: "PUT", - url: `/fhir/${resourceType}/${resourceId}`, - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: resource, + return client.update({ + type: resourceType, + id: resourceId, + resource: resource, }); }, onSuccess: () => { @@ -126,7 +124,7 @@ const VersionDiffDialog = ({ ); onOpenChange("hidden"); }, - onError: utils.onError(), + onError: Utils.onMutationError, }); return ( @@ -173,12 +171,14 @@ const VersionViewDialog = ({ openState, onOpenStateChange, }: { - resource: HistoryEntryResource; + resource: Resource; resourceType: string; resourceId: string; openState: OpenState; onOpenStateChange: (open: OpenState) => void; }) => { + const client = useAidboxClient(); + const queryClient = useQueryClient(); const [mode, setMode] = React.useState("json"); @@ -186,14 +186,10 @@ const VersionViewDialog = ({ const mutation = useMutation({ mutationFn: (resource: string) => { - return AidboxCallWithMeta({ - method: "PUT", - url: `/fhir/${resourceType}/${resourceId}`, - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: resource, + return client.update({ + type: resourceType, + id: resourceId, + resource: resource, }); }, onSuccess: () => { @@ -211,7 +207,7 @@ const VersionViewDialog = ({ ); onOpenStateChange("hidden"); }, - onError: utils.onError(), + onError: Utils.onMutationError, }); const indent = 2; @@ -290,7 +286,7 @@ function VersionIdCell({ versionId, id, }: { - resource: HistoryEntryResource; + resource: Resource; resourceType: string; versionId: string; id: string; @@ -327,8 +323,8 @@ function AffectedCell({ previousResource, currentResource, }: { - previousResource: HistoryEntryResource | null; - currentResource: HistoryEntryResource; + previousResource: Resource | null; + currentResource: Resource; resourceType: string; id: string; affected: Set<(string | number)[]>; @@ -366,8 +362,8 @@ function AffectedCell({ } const calculateAffectedAttributes = ( - previous: HistoryEntryResource | null, - current: HistoryEntryResource, + previous: Resource | null, + current: Resource, ) => { if (!previous) return new Set<(string | number)[]>(); @@ -422,7 +418,9 @@ const calculateAffectedAttributes = ( }; export const VersionsTab = ({ id, resourceType }: VersionsTabProps) => { - const [history, setHistory] = React.useState(); + const client = useAidboxClient(); + + const [history, setHistory] = React.useState(); const { data: historyData, @@ -431,7 +429,7 @@ export const VersionsTab = ({ id, resourceType }: VersionsTabProps) => { } = useQuery({ queryKey: [pageId, resourceType, id, "history"], queryFn: async () => { - return await fetchResourceHistory(resourceType, id); + return await fetchResourceHistory(client, resourceType, id); }, }); @@ -487,42 +485,67 @@ export const VersionsTab = ({ id, resourceType }: VersionsTabProps) => { }, ]; - const historyWithHistory = history.entry - .sort( - (a: HistoryEntry, b: HistoryEntry) => - parseInt(a.resource.meta.versionId) - - parseInt(b.resource.meta.versionId), - ) - .reduce( - ( - acc: { - result: historyWithHistoryProps[]; - prev: HistoryEntryResource | null; + try { + const historyWithHistory = history.entry + ?.map((x: BundleEntry) => { + const versionId = x?.resource?.meta?.versionId; + if (versionId === undefined) + throw Error("No version ID in history item", { cause: x }); + + return { ...x, versionId }; + }) + .sort( + (a, b) => parseInt(a.versionId || "0") - parseInt(b.versionId || "0"), + ) + .reduce( + ( + acc: { + result: historyWithHistoryProps[]; + prev: Resource | null; + }, + entry, + ) => { + const resource = entry.resource; + if (resource === undefined) + throw Error("History item is missing the resource", { + cause: resource, + }); + if (!entry?.response?.status) + throw Error("History item is missing a response status", { + cause: entry, + }); + + acc.result.push({ + versionId: entry.versionId, + date: resource.meta?.lastUpdated + ? new Date(resource.meta.lastUpdated).toLocaleString() + : "-", + status: prettyStatus(entry?.response?.status), + resourceCurrent: resource, + resourcePrevious: acc.prev, + affected: calculateAffectedAttributes(acc.prev, resource), + }); + acc.prev = resource; + return acc; }, - entry, - ) => { - const resource = entry.resource; - acc.result.push({ - versionId: resource.meta.versionId, - date: resource.meta.lastUpdated - ? new Date(resource.meta.lastUpdated).toLocaleString() - : "-", - status: prettyStatus(entry.response.status), - resourceCurrent: resource, - resourcePrevious: acc.prev, - affected: calculateAffectedAttributes(acc.prev, resource), - }); - acc.prev = resource; - return acc; - }, - { result: [], prev: null }, - ); + { result: [], prev: null }, + ); - return ( - - ); + return ( + + ); + } catch { + return ( +
+
+
Error loading version list
+
ID: {id}
+
+
+ ); + } }; diff --git a/src/components/ViewDefinition/code-editor-menubar.tsx b/src/components/ViewDefinition/code-editor-menubar.tsx index c53428c..cbebfe0 100644 --- a/src/components/ViewDefinition/code-editor-menubar.tsx +++ b/src/components/ViewDefinition/code-editor-menubar.tsx @@ -13,7 +13,9 @@ export const CodeEditorFormatSelect = ({ onModeChange(value as "json" | "yaml")} + onValueChange={(value) => + onModeChange(value as Types.ViewDefinitionEditorMode) + } > JSON YAML diff --git a/src/components/ViewDefinition/editor-code-tab-content.tsx b/src/components/ViewDefinition/editor-code-tab-content.tsx index f5fd243..b08dbfc 100644 --- a/src/components/ViewDefinition/editor-code-tab-content.tsx +++ b/src/components/ViewDefinition/editor-code-tab-content.tsx @@ -1,3 +1,4 @@ +import type { ViewDefinition } from "@aidbox-ui/fhir-types/org-sql-on-fhir-ig"; import * as HSComp from "@health-samurai/react-components"; import * as yaml from "js-yaml"; import React from "react"; @@ -7,7 +8,7 @@ import { ViewDefinitionContext, ViewDefinitionResourceTypeContext, } from "./page"; -import type { ViewDefinition, ViewDefinitionEditorMode } from "./types"; +import type { ViewDefinitionEditorMode } from "./types"; export const ViewDefinitionCodeEditor = ({ codeMode, diff --git a/src/components/ViewDefinition/editor-form-tab-content.tsx b/src/components/ViewDefinition/editor-form-tab-content.tsx index 0a1a679..b784a07 100644 --- a/src/components/ViewDefinition/editor-form-tab-content.tsx +++ b/src/components/ViewDefinition/editor-form-tab-content.tsx @@ -1,3 +1,11 @@ +import type { CanonicalResource } from "@aidbox-ui/fhir-types/hl7-fhir-r5-core"; +import type { + ViewDefinition, + ViewDefinitionConstant, + ViewDefinitionSelect, + ViewDefinitionSelectColumn, + ViewDefinitionWhere, +} from "@aidbox-ui/fhir-types/org-sql-on-fhir-ig"; import { Button, Checkbox, @@ -35,7 +43,6 @@ import React, { } from "react"; import { useDebounce, useLocalStorage } from "../../hooks"; import { ViewDefinitionContext } from "./page"; -import type * as Types from "./types"; type ItemMeta = { type: @@ -78,112 +85,137 @@ type ItemMeta = { whereData?: WhereItem; }; -interface ConstantItem { - id: string; - name: string; - valueString: string; -} - -interface WhereItem { - id: string; - path: string; -} - -interface ColumnItem { - id: string; - name: string; - path: string; -} - -interface SelectItemInternal { - id: string; +type ConstantItem = ViewDefinitionConstant & { + nodeId: string; +}; + +type WhereItem = ViewDefinitionWhere & { + nodeId: string; +}; + +type ColumnItem = ViewDefinitionSelectColumn & { + nodeId: string; +}; + +type SelectItemInternal = ViewDefinitionSelect & { + nodeId: string; type: "column" | "forEach" | "forEachOrNull" | "unionAll"; - columns?: ColumnItem[]; + column?: ColumnItem[]; expression?: string; children?: SelectItemInternal[]; -} +}; // Helper functions + +const parseColumn = (id: string, column: ViewDefinitionSelectColumn[]) => { + return { + nodeId: id, + type: "column" as const, + column: column.map((c, idx) => ({ + nodeId: `${id}-col-${idx}-${crypto.randomUUID()}`, + name: c.name || "", + path: c.path || "", + })), + }; +}; + +const parseForEach = ( + id: string, + forEach: string, + select: ViewDefinitionSelect[] | undefined, +) => { + return { + nodeId: id, + type: "forEach" as const, + expression: forEach, + children: select ? parseSelectItems(select, `${id}-`) : [], + }; +}; + +const parseForEachOrNull = ( + id: string, + forEachOrNull: string, + select: ViewDefinitionSelect[] | undefined, +) => { + return { + nodeId: id, + type: "forEachOrNull" as const, + expression: forEachOrNull, + children: select ? parseSelectItems(select, `${id}-`) : [], + }; +}; + +const parseUnionAll = ( + id: string, + unionAll: ViewDefinitionSelect[] | undefined, +) => { + return { + nodeId: id, + type: "unionAll" as const, + children: unionAll ? parseSelectItems(unionAll, `${id}-`) : [], + }; +}; + const parseSelectItems = ( - items: Types.ViewDefinitionSelectItem[], + items: ViewDefinitionSelect[], parentId = "", ): SelectItemInternal[] => { - return items - .map((item, index) => { - const id = `${parentId}select-${index}-${crypto.randomUUID()}`; - - if (item.column) { - return { - id, - type: "column" as const, - columns: item.column.map((c, idx) => ({ - id: `${id}-col-${idx}-${crypto.randomUUID()}`, - name: c.name || "", - path: c.path || "", - })), - }; - } else if (item.forEach !== undefined) { - return { - id, - type: "forEach" as const, - expression: item.forEach, - children: item.select ? parseSelectItems(item.select, `${id}-`) : [], - }; - } else if (item.forEachOrNull !== undefined) { - return { - id, - type: "forEachOrNull" as const, - expression: item.forEachOrNull, - children: item.select ? parseSelectItems(item.select, `${id}-`) : [], - }; - } else if (item.unionAll) { - return { - id, - type: "unionAll" as const, - children: parseSelectItems(item.unionAll, `${id}-`), - }; - } - return null; - }) - .filter(Boolean) as SelectItemInternal[]; + return items.flatMap((item, index) => { + const id = `${parentId}select-${index}-${crypto.randomUUID()}`; + if (item.column) return parseColumn(id, item.column); + else if (item.forEach) return parseForEach(id, item.forEach, item.select); + else if (item.forEachOrNull) + return parseForEachOrNull(id, item.forEachOrNull, item.select); + else if (item.unionAll) return parseUnionAll(id, item.unionAll); + else return []; + }); +}; + +const buildColumn = (columns: ColumnItem[]) => { + return { + column: columns.map((col) => ({ + name: col.name, + path: col.path, + })), + }; +}; + +const buildForEach = ({ expression, children }: SelectItemInternal) => { + const result: ViewDefinitionSelect = { + forEach: expression || "", + }; + if (children && children.length > 0) { + result.select = buildSelectArray(children); + } + return result; +}; + +const buildForEachOrNull = ({ expression, children }: SelectItemInternal) => { + const result: ViewDefinitionSelect = { + forEachOrNull: expression || "", + }; + if (children && children.length > 0) { + result.select = buildSelectArray(children); + } + return result; +}; + +const buildUnionAll = ({ children }: SelectItemInternal) => { + return { + unionAll: children ? buildSelectArray(children) : [], + }; }; const buildSelectArray = ( items: SelectItemInternal[], -): Types.ViewDefinitionSelectItem[] => { - return items - .map((item) => { - if (item.type === "column" && item.columns) { - return { - column: item.columns.map((col) => ({ - name: col.name, - path: col.path, - })), - }; - } else if (item.type === "forEach") { - const result: Types.ViewDefinitionSelectItem = { - forEach: item.expression || "", - }; - if (item.children && item.children.length > 0) { - result.select = buildSelectArray(item.children); - } - return result; - } else if (item.type === "forEachOrNull") { - const result: Types.ViewDefinitionSelectItem = { - forEachOrNull: item.expression || "", - }; - if (item.children && item.children.length > 0) { - result.select = buildSelectArray(item.children); - } - return result; - } else if (item.type === "unionAll") { - return { - unionAll: item.children ? buildSelectArray(item.children) : [], - }; - } - return null; - }) - .filter(Boolean) as Types.ViewDefinitionSelectItem[]; +): ViewDefinitionSelect[] => { + return items.flatMap((item) => { + if (item.type === "column" && item.column) return buildColumn(item.column); + else if (item.type === "forEach") return buildForEach(item); + else if (item.type === "forEachOrNull") return buildForEachOrNull(item); + else if (item.type === "unionAll") return buildUnionAll(item); + else return []; + }); }; const findPath = ( @@ -192,11 +224,11 @@ const findPath = ( path: string[] = [], ): string[] | null => { for (const item of items) { - if (item.id === targetId) { + if (item.nodeId === targetId) { return path; } if (item.children) { - const result = findPath(item.children, targetId, [...path, item.id]); + const result = findPath(item.children, targetId, [...path, item.nodeId]); if (result) return result; } } @@ -270,7 +302,7 @@ export const FormTabContent = () => { ) { const constantsWithIds = viewDefinition.constant.map( (c, index: number) => ({ - id: `constant-${index}-${crypto.randomUUID()}`, + nodeId: `constant-${index}-${crypto.randomUUID()}`, name: c.name || "", valueString: c.valueString || "", }), @@ -287,7 +319,7 @@ export const FormTabContent = () => { viewDefinition.where.length > 0 ) { const whereWithIds = viewDefinition.where.map((w, index: number) => ({ - id: `where-${index}-${crypto.randomUUID()}`, + nodeId: `where-${index}-${crypto.randomUUID()}`, path: w.path || "", })); setWhereConditions(whereWithIds); @@ -309,18 +341,7 @@ export const FormTabContent = () => { ( updatedConstants?: ConstantItem[], updatedWhere?: WhereItem[], - updatedFields?: { - name?: string; - title?: string; - description?: string; - status?: string; - url?: string; - publisher?: string; - copyright?: string; - experimental?: boolean; - fhirVersion?: string[] | undefined; - identifier?: { system?: string; value?: string }[]; - }, + updatedFields?: Partial, updatedSelectItems?: SelectItemInternal[], ) => { if (viewDefinition) { @@ -335,7 +356,7 @@ export const FormTabContent = () => { const selectArray = buildSelectArray(updatedSelectItems || selectItems); - const updatedViewDef = { + const updatedViewDef: ViewDefinition = { ...viewDefinition, ...(updatedFields || {}), }; @@ -352,11 +373,7 @@ export const FormTabContent = () => { delete updatedViewDef.where; } - if (selectArray.length > 0) { - updatedViewDef.select = selectArray; - } else { - delete updatedViewDef.select; - } + updatedViewDef.select = selectArray; viewDefinitionContext.setViewDefinition(updatedViewDef); } @@ -373,7 +390,7 @@ export const FormTabContent = () => { // Function to add a new constant const addConstant = () => { const newConstant = { - id: `constant-${constants.length}-${crypto.randomUUID()}`, + nodeId: `constant-${constants.length}-${crypto.randomUUID()}`, name: "", valueString: "", }; @@ -381,7 +398,7 @@ export const FormTabContent = () => { setConstants(updatedConstants); const newCollapsedIds = collapsedItemIds.filter( - (id) => id !== newConstant.id && id !== "_constant", + (id) => id !== newConstant.nodeId && id !== "_constant", ); setCollapsedItemIds(newCollapsedIds); @@ -395,7 +412,7 @@ export const FormTabContent = () => { value: string, ) => { const updatedConstants = constants.map((c) => - c.id === id ? { ...c, [field]: value } : c, + c.nodeId === id ? { ...c, [field]: value } : c, ); setConstants(updatedConstants); updateViewDefinition(updatedConstants); @@ -403,7 +420,7 @@ export const FormTabContent = () => { // Function to remove a constant const removeConstant = (id: string) => { - const updatedConstants = constants.filter((c) => c.id !== id); + const updatedConstants = constants.filter((c) => c.nodeId !== id); setConstants(updatedConstants); updateViewDefinition(updatedConstants); }; @@ -411,14 +428,14 @@ export const FormTabContent = () => { // Function to add a new where condition const addWhereCondition = () => { const newWhere = { - id: `where-${whereConditions.length}-${crypto.randomUUID()}`, + nodeId: `where-${whereConditions.length}-${crypto.randomUUID()}`, path: "", }; const updatedWhere = [...whereConditions, newWhere]; setWhereConditions(updatedWhere); const newCollapsedIds = collapsedItemIds.filter( - (id) => id !== newWhere.id && id !== "_where", + (id) => id !== newWhere.nodeId && id !== "_where", ); setCollapsedItemIds(newCollapsedIds); @@ -428,7 +445,7 @@ export const FormTabContent = () => { // Function to update a specific where condition const updateWhereCondition = (id: string, path: string) => { const updatedWhere = whereConditions.map((w) => - w.id === id ? { ...w, path } : w, + w.nodeId === id ? { ...w, path } : w, ); setWhereConditions(updatedWhere); updateViewDefinition(undefined, updatedWhere); @@ -436,7 +453,7 @@ export const FormTabContent = () => { // Function to remove a where condition const removeWhereCondition = (id: string) => { - const updatedWhere = whereConditions.filter((w) => w.id !== id); + const updatedWhere = whereConditions.filter((w) => w.nodeId !== id); setWhereConditions(updatedWhere); updateViewDefinition(undefined, updatedWhere); }; @@ -457,7 +474,7 @@ export const FormTabContent = () => { }; // Function to update status field - const updateStatus = (status: string) => { + const updateStatus = (status: CanonicalResource["status"]) => { updateViewDefinition(undefined, undefined, { status }); }; @@ -482,9 +499,10 @@ export const FormTabContent = () => { }; // Function to update fhirVersion field - const updateFhirVersions = (fhirVersions: string[]) => { + const updateFhirVersions = (fhirVersions: ViewDefinition["fhirVersion"]) => { updateViewDefinition(undefined, undefined, { - fhirVersion: fhirVersions.length > 0 ? fhirVersions : undefined, + fhirVersion: + fhirVersions && fhirVersions.length > 0 ? fhirVersions : undefined, }); }; @@ -494,14 +512,14 @@ export const FormTabContent = () => { parentPath?: string[], ) => { const newItem: SelectItemInternal = { - id: `${type}-${Date.now()}-${crypto.randomUUID()}`, + nodeId: `${type}-${Date.now()}-${crypto.randomUUID()}`, type, }; if (type === "column") { - newItem.columns = [ + newItem.column = [ { - id: `col-${Date.now()}-${crypto.randomUUID()}`, + nodeId: `col-${Date.now()}-${crypto.randomUUID()}`, name: "", path: "", }, @@ -513,23 +531,23 @@ export const FormTabContent = () => { newItem.children = []; } - const idsToRemove = [newItem.id, "_select"]; + const idsToRemove = [newItem.nodeId, "_select"]; if (parentPath) { idsToRemove.push(...parentPath); } - if (type === "column" && newItem.columns) { - newItem.columns.forEach((col) => { - idsToRemove.push(col.id); + if (type === "column" && newItem.column) { + newItem.column.forEach((col: ColumnItem) => { + idsToRemove.push(col.nodeId); }); - idsToRemove.push(`${newItem.id}_add_column`); + idsToRemove.push(`${newItem.nodeId}_add_column`); } else if ( type === "forEach" || type === "forEachOrNull" || type === "unionAll" ) { - idsToRemove.push(`${newItem.id}_add_select`); + idsToRemove.push(`${newItem.nodeId}_add_select`); } const newCollapsedIds = collapsedItemIds.filter( @@ -542,7 +560,7 @@ export const FormTabContent = () => { const updatedItems = JSON.parse(JSON.stringify(selectItems)); let target = updatedItems; for (const id of parentPath) { - const item = target.find((i: SelectItemInternal) => i.id === id); + const item = target.find((i: SelectItemInternal) => i.nodeId === id); if (item?.children) { target = item.children; } @@ -567,13 +585,13 @@ export const FormTabContent = () => { items: SelectItemInternal[], ): SelectItemInternal[] => { return items.map((item) => { - if (item.id === selectItemId && item.type === "column") { + if (item.nodeId === selectItemId && item.type === "column") { return { ...item, - columns: [ - ...(item.columns || []), + column: [ + ...(item.column || []), { - id: newColumnId, + nodeId: newColumnId, name: "", path: "", }, @@ -617,11 +635,11 @@ export const FormTabContent = () => { items: SelectItemInternal[], ): SelectItemInternal[] => { return items.map((item) => { - if (item.id === selectItemId && item.columns) { + if (item.nodeId === selectItemId && item.column) { return { ...item, - columns: item.columns.map((col) => - col.id === columnId ? { ...col, [field]: value } : col, + column: item.column.map((col: ColumnItem) => + col.nodeId === columnId ? { ...col, [field]: value } : col, ), }; } @@ -644,7 +662,7 @@ export const FormTabContent = () => { ): SelectItemInternal[] => { return items.map((item) => { if ( - item.id === selectItemId && + item.nodeId === selectItemId && (item.type === "forEach" || item.type === "forEachOrNull") ) { return { ...item, expression }; @@ -667,10 +685,12 @@ export const FormTabContent = () => { items: SelectItemInternal[], ): SelectItemInternal[] => { return items.map((item) => { - if (item.id === selectItemId && item.columns) { + if (item.nodeId === selectItemId && item.column) { return { ...item, - columns: item.columns.filter((col) => col.id !== columnId), + column: item.column.filter( + (col: ColumnItem) => col.nodeId !== columnId, + ), }; } if (item.children) { @@ -689,7 +709,7 @@ export const FormTabContent = () => { const removeSelectItem = (itemId: string) => { const removeItem = (items: SelectItemInternal[]): SelectItemInternal[] => { return items - .filter((item) => item.id !== itemId) + .filter((item) => item.nodeId !== itemId) .map((item) => { if (item.children) { return { ...item, children: removeItem(item.children) }; @@ -706,11 +726,11 @@ export const FormTabContent = () => { // Dynamic tree generation based on current constants and where conditions const tree: Record> = useMemo(() => { const constantChildren = - constants.length > 0 ? constants.map((c) => c.id) : []; + constants.length > 0 ? constants.map((c) => c.nodeId) : []; constantChildren.push("_constant_add"); const whereChildren = - whereConditions.length > 0 ? whereConditions.map((w) => w.id) : []; + whereConditions.length > 0 ? whereConditions.map((w) => w.nodeId) : []; whereChildren.push("_where_add"); const treeStructure: Record> = {}; @@ -720,10 +740,10 @@ export const FormTabContent = () => { const children: string[] = []; items.forEach((item) => { - children.push(item.id); + children.push(item.nodeId); const currentItem: TreeViewItem = { - name: item.id, + name: item.nodeId, meta: { type: `select-${item.type}`, selectData: item, @@ -731,27 +751,27 @@ export const FormTabContent = () => { children: [], }; - treeStructure[item.id] = currentItem; + treeStructure[item.nodeId] = currentItem; - if (item.type === "column" && item.columns) { + if (item.type === "column" && item.column) { const columnChildren: string[] = []; - item.columns.forEach((col) => { - columnChildren.push(col.id); - treeStructure[col.id] = { - name: col.id, + item.column.forEach((col: ColumnItem) => { + columnChildren.push(col.nodeId); + treeStructure[col.nodeId] = { + name: col.nodeId, meta: { type: "column-item", columnData: col, - selectItemId: item.id, + selectItemId: item.nodeId, }, }; }); - columnChildren.push(`${item.id}_add_column`); - treeStructure[`${item.id}_add_column`] = { - name: `${item.id}_add_column`, + columnChildren.push(`${item.nodeId}_add_column`); + treeStructure[`${item.nodeId}_add_column`] = { + name: `${item.nodeId}_add_column`, meta: { type: "column-add", - selectItemId: item.id, + selectItemId: item.nodeId, }, }; currentItem.children = columnChildren; @@ -767,12 +787,12 @@ export const FormTabContent = () => { nodeChildren.push(...nestedChildren); } - nodeChildren.push(`${item.id}_add_select`); - treeStructure[`${item.id}_add_select`] = { - name: `${item.id}_add_select`, + nodeChildren.push(`${item.nodeId}_add_select`); + treeStructure[`${item.nodeId}_add_select`] = { + name: `${item.nodeId}_add_select`, meta: { type: "select-add-nested", - parentId: item.id, + parentId: item.nodeId, }, }; @@ -923,8 +943,8 @@ export const FormTabContent = () => { Object.assign(treeStructure, newTreeStructure); constants.forEach((constant, index) => { - treeStructure[constant.id] = { - name: constant.id, + treeStructure[constant.nodeId] = { + name: constant.nodeId, meta: { type: "constant-value", lastNode: index === constants.length - 1, @@ -934,8 +954,8 @@ export const FormTabContent = () => { }); whereConditions.forEach((whereCondition, index) => { - treeStructure[whereCondition.id] = { - name: whereCondition.id, + treeStructure[whereCondition.nodeId] = { + name: whereCondition.nodeId, meta: { type: "where-value", lastNode: index === whereConditions.length - 1, @@ -955,7 +975,6 @@ export const FormTabContent = () => { item.getItemData().children = newChildren; const itemId = item.getId(); - const _itemMeta = item.getItemData()?.meta; // Handle reordering of constants if (itemId === "_constant") { @@ -966,7 +985,7 @@ export const FormTabContent = () => { const reorderedConstants: ConstantItem[] = []; for (const id of reorderedConstantIds) { - const constant = constants.find((c) => c.id === id); + const constant = constants.find((c) => c.nodeId === id); if (constant) { reorderedConstants.push(constant); } @@ -985,7 +1004,7 @@ export const FormTabContent = () => { const reorderedWhere: WhereItem[] = []; for (const id of reorderedWhereIds) { - const where = whereConditions.find((w) => w.id === id); + const where = whereConditions.find((w) => w.nodeId === id); if (where) { reorderedWhere.push(where); } @@ -1010,7 +1029,7 @@ export const FormTabContent = () => { ): SelectItemInternal[] => { const reordered: SelectItemInternal[] = []; for (const id of orderedIds) { - const item = items.find((i) => i.id === id); + const item = items.find((i) => i.nodeId === id); if (item) { reordered.push(item); } @@ -1037,8 +1056,8 @@ export const FormTabContent = () => { let wasUpdated = false; const updatedItems = items.map((item) => { // Check if this item matches - if (item.id === itemId) { - if (item.type === "column" && item.columns) { + if (item.nodeId === itemId) { + if (item.type === "column" && item.column) { // Reorder columns const reorderedColumnIds = newChildren.filter( (id) => !id.endsWith("_add_column"), @@ -1046,15 +1065,17 @@ export const FormTabContent = () => { const reorderedColumns: ColumnItem[] = []; for (const id of reorderedColumnIds) { - const column = item.columns.find((c) => c.id === id); + const column = item.column.find( + (c: ColumnItem) => c.nodeId === id, + ); if (column) { reorderedColumns.push(column); } } - if (reorderedColumns.length === item.columns.length) { + if (reorderedColumns.length === item.column.length) { wasUpdated = true; - return { ...item, columns: reorderedColumns }; + return { ...item, column: reorderedColumns }; } } else if ( (item.type === "forEach" || @@ -1069,7 +1090,7 @@ export const FormTabContent = () => { const reorderedChildren: SelectItemInternal[] = []; for (const id of reorderedChildIds) { - const child = item.children.find((c) => c.id === id); + const child = item.children.find((c) => c.nodeId === id); if (child) { reorderedChildren.push(child); } @@ -1171,8 +1192,6 @@ export const FormTabContent = () => { const customItemView = (item: ItemInstance>) => { const metaType = item.getItemData()?.meta?.type; - const _itemId = item.getId(); - const _itemProps = item.getProps(); // Helper function to render drag handle for draggable items const renderDragHandle = () => { @@ -1249,7 +1268,9 @@ export const FormTabContent = () => {