From 3ddbac71a35f418c895bbe7d543d9ca431227021 Mon Sep 17 00:00:00 2001 From: Adrian Escutia Soto Date: Sun, 16 Mar 2025 13:52:25 -0500 Subject: [PATCH 01/50] Refactor: Add slug to Application, Project, and Server interfaces --- public/intent-base-ai.png:Zone.Identifier | 4 ++++ src/types/application.ts | 2 +- src/types/project.ts | 3 +++ src/types/server.ts | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 public/intent-base-ai.png:Zone.Identifier diff --git a/public/intent-base-ai.png:Zone.Identifier b/public/intent-base-ai.png:Zone.Identifier new file mode 100644 index 0000000..629ec8c --- /dev/null +++ b/public/intent-base-ai.png:Zone.Identifier @@ -0,0 +1,4 @@ +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=https://lmarena.ai/ +HostUrl=https://lmarena.ai/file=/tmp/gradio/72ebb36577c66b991502a062fdc7d90a7971d22f494f81603fafb340e413ba67/image.webp diff --git a/src/types/application.ts b/src/types/application.ts index 755af22..d9c7fd2 100644 --- a/src/types/application.ts +++ b/src/types/application.ts @@ -6,7 +6,7 @@ export interface Application { name: string; description?: string; category?: string; - slug?: string; + slug: string; status: 'active' | 'inactive' | 'archived'; tags?: string[]; favorite?: boolean; diff --git a/src/types/project.ts b/src/types/project.ts index d4f2474..bff0d44 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -1,6 +1,9 @@ export interface Project { id: string; + slug: string; + organization_id: string; + is_public: boolean; name: string; description: string | null; tools_count?: number; diff --git a/src/types/server.ts b/src/types/server.ts index 94db02d..68ce64d 100644 --- a/src/types/server.ts +++ b/src/types/server.ts @@ -5,6 +5,7 @@ export interface Server { id: string; name: string; slug: string; + is_public: boolean; description: string; type: string; status: ApplicationStatus; From 0eadb35ebcef000f10e44045e85b1dd88c4d9451 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:38:32 +0000 Subject: [PATCH 02/50] Implement drag and drop functionality Implement drag and drop functionality for applications and tools in project details using "/@hello-pangea/dnd" library. --- package-lock.json | 228 +++++++++++++++++- package.json | 1 + .../projects/detail/DraggableResourceList.tsx | 175 ++++++++++++++ .../projects/detail/ProjectTabs.tsx | 79 ++++-- src/hooks/useProjectApplications.tsx | 116 +++++++++ src/hooks/useProjectTools.tsx | 116 +++++++++ 6 files changed, 689 insertions(+), 26 deletions(-) create mode 100644 src/components/projects/detail/DraggableResourceList.tsx create mode 100644 src/hooks/useProjectApplications.tsx create mode 100644 src/hooks/useProjectTools.tsx diff --git a/package-lock.json b/package-lock.json index 210d4a9..509f723 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "vite_react_shadcn_ts", "version": "0.1.0", "dependencies": { + "@hello-pangea/dnd": "^16.6.0", "@hookform/resolvers": "^3.9.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", @@ -87,6 +88,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -771,6 +773,25 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@hello-pangea/dnd": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-16.6.0.tgz", + "integrity": "sha512-vfZ4GydqbtUPXSLfAvKvXQ6xwRzIjUSjVU0Sx+70VOhc2xx6CdmJXJ8YhH70RpbTUGjxctslQTHul9sIOxCfFQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.24.1", + "css-box-model": "^1.2.1", + "memoize-one": "^6.0.0", + "raf-schd": "^4.0.3", + "react-redux": "^8.1.3", + "redux": "^4.2.1", + "use-memo-one": "^1.1.3" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@hookform/resolvers": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", @@ -850,6 +871,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -867,6 +889,7 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -881,6 +904,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -890,6 +914,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -899,12 +924,14 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -950,6 +977,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -2956,6 +2984,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", + "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2989,14 +3027,12 @@ "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", - "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -3007,12 +3043,18 @@ "version": "18.3.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", @@ -3298,6 +3340,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3310,6 +3353,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3325,12 +3369,14 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -3344,6 +3390,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -3422,12 +3469,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3506,6 +3555,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -3552,6 +3602,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -3576,6 +3627,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -3625,6 +3677,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3637,6 +3690,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/commander": { @@ -3674,6 +3728,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3684,10 +3739,20 @@ "node": ">= 8" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -3874,6 +3939,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, "license": "Apache-2.0" }, "node_modules/dir-glob": { @@ -3892,6 +3958,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, "license": "MIT" }, "node_modules/dom-helpers": { @@ -3908,6 +3975,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -3955,6 +4023,7 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/esbuild": { @@ -4392,6 +4461,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -4436,6 +4506,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4450,6 +4521,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4490,6 +4562,7 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -4510,6 +4583,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -4522,6 +4596,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4531,6 +4606,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -4602,6 +4678,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4610,6 +4687,21 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4669,6 +4761,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -4681,6 +4774,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -4705,6 +4799,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4735,12 +4830,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -4756,6 +4853,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -4841,6 +4939,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -4853,6 +4952,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -4932,6 +5032,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, "license": "ISC" }, "node_modules/lucide-react": { @@ -4968,6 +5069,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5007,6 +5114,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -5023,6 +5131,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -5034,6 +5143,7 @@ "version": "3.3.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz", "integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==", + "dev": true, "funding": [ { "type": "github", @@ -5076,6 +5186,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5104,6 +5215,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -5172,6 +5284,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/pako": { @@ -5206,6 +5319,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5215,12 +5329,14 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -5246,6 +5362,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -5264,6 +5381,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5273,6 +5391,7 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -5346,6 +5465,7 @@ "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -5374,6 +5494,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -5391,6 +5512,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -5410,6 +5532,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -5445,6 +5568,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -5470,6 +5594,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -5497,6 +5622,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { @@ -5556,6 +5682,12 @@ ], "license": "MIT" }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "license": "MIT" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -5617,6 +5749,45 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-redux": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", + "hoist-non-react-statics": "^3.3.2", + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-remove-scroll": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", @@ -5755,6 +5926,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -5764,6 +5936,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -5804,6 +5977,15 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -5814,6 +5996,7 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -5940,6 +6123,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -5952,6 +6136,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5961,6 +6146,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -5992,6 +6178,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -6001,6 +6188,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -6019,6 +6207,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6033,6 +6222,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6042,12 +6232,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6060,6 +6252,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -6076,6 +6269,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6088,6 +6282,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6131,6 +6326,7 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -6153,6 +6349,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -6175,6 +6372,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6197,6 +6395,7 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -6243,6 +6442,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -6256,6 +6456,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -6265,6 +6466,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -6335,6 +6537,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, "license": "Apache-2.0" }, "node_modules/tslib": { @@ -6476,6 +6679,15 @@ } } }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", @@ -6511,6 +6723,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/vaul": { @@ -7058,6 +7271,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -7083,6 +7297,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -7101,6 +7316,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -7118,6 +7334,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7127,12 +7344,14 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -7147,6 +7366,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -7159,6 +7379,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7192,6 +7413,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 92c8357..5164bac 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "deploy": "gh-pages -d dist" }, "dependencies": { + "@hello-pangea/dnd": "^16.6.0", "@hookform/resolvers": "^3.9.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", diff --git a/src/components/projects/detail/DraggableResourceList.tsx b/src/components/projects/detail/DraggableResourceList.tsx new file mode 100644 index 0000000..007aae7 --- /dev/null +++ b/src/components/projects/detail/DraggableResourceList.tsx @@ -0,0 +1,175 @@ + +import React from 'react'; +import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Application } from '@/types/application'; +import { AITool } from '@/types/ai-tool'; +import { ApplicationCard } from '@/components/applications/ApplicationCard'; +import { toast } from 'sonner'; +import { Button } from '@/components/ui/button'; + +interface ResourceContainerProps { + id: string; + title: string; + items: Application[] | AITool[]; + resourceType: 'application' | 'tool'; + emptyMessage: string; + emptyIcon: React.ReactNode; + createButtonLabel?: string; + onCreateClick?: () => void; +} + +interface DraggableResourceListProps { + projectId: string; + availableResources: Application[] | AITool[]; + associatedResources: Application[] | AITool[]; + resourceType: 'application' | 'tool'; + onResourceMoved: (resourceId: string, source: string, destination: string) => Promise; + createButtonLabel?: string; + onCreateClick?: () => void; +} + +// A component to render each column (Available or Associated) +const ResourceContainer = ({ + id, + title, + items, + resourceType, + emptyMessage, + emptyIcon, + createButtonLabel, + onCreateClick +}: ResourceContainerProps) => ( + + + {title} + + + {(provided) => ( + + {items.length > 0 ? ( +
+ {items.map((item, index) => ( + + {(provided) => ( +
+ {resourceType === 'application' ? ( + + ) : ( + +

{(item as AITool).name}

+

{(item as AITool).description}

+
+ )} +
+ )} +
+ ))} +
+ ) : ( +
+ {emptyIcon} +

+ {emptyMessage} +

+ {createButtonLabel && onCreateClick && ( + + )} +
+ )} + {provided.placeholder} +
+ )} +
+
+); + +export function DraggableResourceList({ + projectId, + availableResources, + associatedResources, + resourceType, + onResourceMoved, + createButtonLabel, + onCreateClick +}: DraggableResourceListProps) { + const handleDragEnd = async (result: DropResult) => { + const { source, destination, draggableId } = result; + + // Dropped outside a valid droppable area + if (!destination) return; + + // Dropped in the same place + if ( + source.droppableId === destination.droppableId && + source.index === destination.index + ) { + return; + } + + // Handle association or disassociation + try { + await onResourceMoved(draggableId, source.droppableId, destination.droppableId); + + // Show success message + if (source.droppableId === 'available' && destination.droppableId === 'associated') { + toast.success(`${resourceType === 'application' ? 'Application' : 'AI Tool'} associated with project`); + } else if (source.droppableId === 'associated' && destination.droppableId === 'available') { + toast.success(`${resourceType === 'application' ? 'Application' : 'AI Tool'} removed from project`); + } + } catch (error) { + console.error('Error moving resource:', error); + toast.error(`Failed to update ${resourceType} association`); + } + }; + + return ( + +
+ + +
+ :
+ +
+ } + createButtonLabel={createButtonLabel} + onCreateClick={onCreateClick} + /> + + + + + :
+ +
+ } + /> + +
+ ); +} diff --git a/src/components/projects/detail/ProjectTabs.tsx b/src/components/projects/detail/ProjectTabs.tsx index 5e7f620..12ac2d8 100644 --- a/src/components/projects/detail/ProjectTabs.tsx +++ b/src/components/projects/detail/ProjectTabs.tsx @@ -1,14 +1,41 @@ -import { AppWindow, CircuitBoard } from 'lucide-react'; +import { AppWindow, CircuitBoard, Server } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { ResourceTabs } from '../../detail/ResourceTabs'; import { Project } from '@/types/project'; +import { DraggableResourceList } from './DraggableResourceList'; +import { useProjectApplications } from '@/hooks/useProjectApplications'; +import { useProjectTools } from '@/hooks/useProjectTools'; +import { useNavigate } from 'react-router'; interface ProjectTabsProps { project: Project; } export function ProjectTabs({ project }: ProjectTabsProps) { + const navigate = useNavigate(); + const { + availableApplications, + associatedApplications, + isLoading: isLoadingApplications, + handleMoveApplication + } = useProjectApplications(project.id); + + const { + availableTools, + associatedTools, + isLoading: isLoadingTools, + handleMoveTool + } = useProjectTools(project.id); + + const handleCreateApplication = () => { + navigate('/applications/new', { state: { projectId: project.id } }); + }; + + const handleCreateTool = () => { + navigate('/tools/new', { state: { projectId: project.id } }); + }; + return ( Applications will be listed here.

- ) : ( -
- -

No Applications

-

- There are no applications associated with this project yet. -

- + description: 'Manage applications associated with this project', + content: isLoadingApplications ? ( +
+
+ ) : ( + ), }, { @@ -53,18 +83,21 @@ export function ProjectTabs({ project }: ProjectTabsProps) { { value: 'tools', label: 'AI Tools', - description: 'AI tools associated with this project', - content: project.tools_count ? ( -

AI Tools will be listed here.

- ) : ( -
- -

No AI Tools

-

- There are no AI tools associated with this project yet. -

- + description: 'Manage AI tools associated with this project', + content: isLoadingTools ? ( +
+
+ ) : ( + ), }, ]} diff --git a/src/hooks/useProjectApplications.tsx b/src/hooks/useProjectApplications.tsx new file mode 100644 index 0000000..86bcdef --- /dev/null +++ b/src/hooks/useProjectApplications.tsx @@ -0,0 +1,116 @@ + +import { useState, useEffect } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { supabase } from '@/integrations/supabase/client'; +import { Application } from '@/types/application'; +import { toast } from 'sonner'; + +export function useProjectApplications(projectId: string) { + const queryClient = useQueryClient(); + const [availableApplications, setAvailableApplications] = useState([]); + const [associatedApplications, setAssociatedApplications] = useState([]); + + // Fetch all applications + const { data: allApplications, isLoading: isLoadingAll } = useQuery({ + queryKey: ['applications'], + queryFn: async () => { + const { data, error } = await supabase + .from('applications') + .select('*'); + + if (error) throw error; + return data as Application[]; + }, + }); + + // Fetch applications associated with the project + const { data: projectApplications, isLoading: isLoadingAssociated } = useQuery({ + queryKey: ['project-applications', projectId], + queryFn: async () => { + const { data, error } = await supabase + .from('applications') + .select('*') + .eq('project_id', projectId); + + if (error) throw error; + return data as Application[]; + }, + enabled: !!projectId, + }); + + // Associate/disassociate application with project + const { mutateAsync: updateProjectAssociation } = useMutation({ + mutationFn: async ({ + applicationId, + action + }: { + applicationId: string, + action: 'associate' | 'disassociate' + }) => { + if (action === 'associate') { + const { error } = await supabase + .from('applications') + .update({ project_id: projectId }) + .eq('id', applicationId); + + if (error) throw error; + } else { + const { error } = await supabase + .from('applications') + .update({ project_id: null }) + .eq('id', applicationId); + + if (error) throw error; + } + }, + onSuccess: () => { + // Invalidate queries to refetch data + queryClient.invalidateQueries({ queryKey: ['applications'] }); + queryClient.invalidateQueries({ queryKey: ['project-applications', projectId] }); + }, + onError: (error) => { + console.error('Error updating association:', error); + toast.error('Failed to update application association'); + }, + }); + + // Set available and associated applications + useEffect(() => { + if (allApplications && projectApplications) { + // Available applications are those not already associated with the project + const associated = projectApplications || []; + const associatedIds = associated.map(app => app.id); + + setAvailableApplications( + allApplications.filter(app => !associatedIds.includes(app.id)) + ); + setAssociatedApplications(associated); + } + }, [allApplications, projectApplications]); + + // Handle moving application between available and associated + const handleMoveApplication = async ( + applicationId: string, + sourceList: string, + destinationList: string + ) => { + if (sourceList === 'available' && destinationList === 'associated') { + await updateProjectAssociation({ + applicationId, + action: 'associate' + }); + } else if (sourceList === 'associated' && destinationList === 'available') { + await updateProjectAssociation({ + applicationId, + action: 'disassociate' + }); + } + }; + + return { + availableApplications, + associatedApplications, + isLoading: isLoadingAll || isLoadingAssociated, + handleMoveApplication, + }; +} diff --git a/src/hooks/useProjectTools.tsx b/src/hooks/useProjectTools.tsx new file mode 100644 index 0000000..5cb1f00 --- /dev/null +++ b/src/hooks/useProjectTools.tsx @@ -0,0 +1,116 @@ + +import { useState, useEffect } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { supabase } from '@/integrations/supabase/client'; +import { AITool } from '@/types/ai-tool'; +import { toast } from 'sonner'; + +export function useProjectTools(projectId: string) { + const queryClient = useQueryClient(); + const [availableTools, setAvailableTools] = useState([]); + const [associatedTools, setAssociatedTools] = useState([]); + + // Fetch all AI tools + const { data: allTools, isLoading: isLoadingAll } = useQuery({ + queryKey: ['ai-tools'], + queryFn: async () => { + const { data, error } = await supabase + .from('ai_tools') + .select('*'); + + if (error) throw error; + return data as AITool[]; + }, + }); + + // Fetch AI tools associated with the project + const { data: projectTools, isLoading: isLoadingAssociated } = useQuery({ + queryKey: ['project-tools', projectId], + queryFn: async () => { + const { data, error } = await supabase + .from('ai_tools') + .select('*') + .eq('project_id', projectId); + + if (error) throw error; + return data as AITool[]; + }, + enabled: !!projectId, + }); + + // Associate/disassociate AI tool with project + const { mutateAsync: updateToolAssociation } = useMutation({ + mutationFn: async ({ + toolId, + action + }: { + toolId: string, + action: 'associate' | 'disassociate' + }) => { + if (action === 'associate') { + const { error } = await supabase + .from('ai_tools') + .update({ project_id: projectId }) + .eq('id', toolId); + + if (error) throw error; + } else { + const { error } = await supabase + .from('ai_tools') + .update({ project_id: null }) + .eq('id', toolId); + + if (error) throw error; + } + }, + onSuccess: () => { + // Invalidate queries to refetch data + queryClient.invalidateQueries({ queryKey: ['ai-tools'] }); + queryClient.invalidateQueries({ queryKey: ['project-tools', projectId] }); + }, + onError: (error) => { + console.error('Error updating association:', error); + toast.error('Failed to update AI tool association'); + }, + }); + + // Set available and associated tools + useEffect(() => { + if (allTools && projectTools) { + // Available tools are those not already associated with the project + const associated = projectTools || []; + const associatedIds = associated.map(tool => tool.id); + + setAvailableTools( + allTools.filter(tool => !associatedIds.includes(tool.id)) + ); + setAssociatedTools(associated); + } + }, [allTools, projectTools]); + + // Handle moving tool between available and associated + const handleMoveTool = async ( + toolId: string, + sourceList: string, + destinationList: string + ) => { + if (sourceList === 'available' && destinationList === 'associated') { + await updateToolAssociation({ + toolId, + action: 'associate' + }); + } else if (sourceList === 'associated' && destinationList === 'available') { + await updateToolAssociation({ + toolId, + action: 'disassociate' + }); + } + }; + + return { + availableTools, + associatedTools, + isLoading: isLoadingAll || isLoadingAssociated, + handleMoveTool, + }; +} From 631326a2ecc08ec38debc4a9b6dbfdb3f6527df0 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 16 Mar 2025 21:04:02 +0000 Subject: [PATCH 03/50] Run database schema updates Runs the provided SQL script to update the database schema. --- src/hooks/useProjectApplications.tsx | 59 ++++++++++++++++++++++------ src/hooks/useProjectTools.tsx | 7 ++++ src/integrations/supabase/types.ts | 36 +++++++++++++++++ src/types/project-application.ts | 11 ++++++ 4 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 src/types/project-application.ts diff --git a/src/hooks/useProjectApplications.tsx b/src/hooks/useProjectApplications.tsx index 86bcdef..d856f84 100644 --- a/src/hooks/useProjectApplications.tsx +++ b/src/hooks/useProjectApplications.tsx @@ -5,6 +5,13 @@ import { supabase } from '@/integrations/supabase/client'; import { Application } from '@/types/application'; import { toast } from 'sonner'; +export interface ProjectApplication { + id: string; + project_id: string; + application_id: string; + created_at: string; +} + export function useProjectApplications(projectId: string) { const queryClient = useQueryClient(); const [availableApplications, setAvailableApplications] = useState([]); @@ -23,22 +30,43 @@ export function useProjectApplications(projectId: string) { }, }); - // Fetch applications associated with the project + // Fetch project_applications join records + const { data: projectApplicationsJoin, isLoading: isLoadingJoin } = useQuery({ + queryKey: ['project-applications-join', projectId], + queryFn: async () => { + const { data, error } = await supabase + .from('project_applications') + .select('*') + .eq('project_id', projectId); + + if (error) throw error; + return data as ProjectApplication[]; + }, + enabled: !!projectId, + }); + + // Fetch applications associated with the project through the join table const { data: projectApplications, isLoading: isLoadingAssociated } = useQuery({ queryKey: ['project-applications', projectId], queryFn: async () => { + if (!projectApplicationsJoin || projectApplicationsJoin.length === 0) { + return [] as Application[]; + } + + const applicationIds = projectApplicationsJoin.map(join => join.application_id); + const { data, error } = await supabase .from('applications') .select('*') - .eq('project_id', projectId); + .in('id', applicationIds); if (error) throw error; return data as Application[]; }, - enabled: !!projectId, + enabled: !!projectId && !!projectApplicationsJoin, }); - // Associate/disassociate application with project + // Associate/disassociate application with project using the join table const { mutateAsync: updateProjectAssociation } = useMutation({ mutationFn: async ({ applicationId, @@ -48,24 +76,31 @@ export function useProjectApplications(projectId: string) { action: 'associate' | 'disassociate' }) => { if (action === 'associate') { + // Add a record to the join table const { error } = await supabase - .from('applications') - .update({ project_id: projectId }) - .eq('id', applicationId); + .from('project_applications') + .insert({ + project_id: projectId, + application_id: applicationId + }); if (error) throw error; } else { + // Remove the record from the join table const { error } = await supabase - .from('applications') - .update({ project_id: null }) - .eq('id', applicationId); + .from('project_applications') + .delete() + .match({ + project_id: projectId, + application_id: applicationId + }); if (error) throw error; } }, onSuccess: () => { // Invalidate queries to refetch data - queryClient.invalidateQueries({ queryKey: ['applications'] }); + queryClient.invalidateQueries({ queryKey: ['project-applications-join', projectId] }); queryClient.invalidateQueries({ queryKey: ['project-applications', projectId] }); }, onError: (error) => { @@ -110,7 +145,7 @@ export function useProjectApplications(projectId: string) { return { availableApplications, associatedApplications, - isLoading: isLoadingAll || isLoadingAssociated, + isLoading: isLoadingAll || isLoadingAssociated || isLoadingJoin, handleMoveApplication, }; } diff --git a/src/hooks/useProjectTools.tsx b/src/hooks/useProjectTools.tsx index 5cb1f00..1adf24c 100644 --- a/src/hooks/useProjectTools.tsx +++ b/src/hooks/useProjectTools.tsx @@ -5,6 +5,13 @@ import { supabase } from '@/integrations/supabase/client'; import { AITool } from '@/types/ai-tool'; import { toast } from 'sonner'; +interface ProjectTool { + id: string; + project_id: string; + ai_tool_id: string; + created_at: string; +} + export function useProjectTools(projectId: string) { const queryClient = useQueryClient(); const [availableTools, setAvailableTools] = useState([]); diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 5ecb185..ce5cf48 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -478,6 +478,42 @@ export type Database = { } Relationships: [] } + project_applications: { + Row: { + application_id: string + created_at: string + id: string + project_id: string + } + Insert: { + application_id: string + created_at?: string + id?: string + project_id: string + } + Update: { + application_id?: string + created_at?: string + id?: string + project_id?: string + } + Relationships: [ + { + foreignKeyName: "project_applications_application_id_fkey" + columns: ["application_id"] + isOneToOne: false + referencedRelation: "applications" + referencedColumns: ["id"] + }, + { + foreignKeyName: "project_applications_project_id_fkey" + columns: ["project_id"] + isOneToOne: false + referencedRelation: "projects" + referencedColumns: ["id"] + }, + ] + } projects: { Row: { applications_count: number | null diff --git a/src/types/project-application.ts b/src/types/project-application.ts new file mode 100644 index 0000000..d1fbe28 --- /dev/null +++ b/src/types/project-application.ts @@ -0,0 +1,11 @@ + +/** + * Represents a join table record between projects and applications + * enabling many-to-many relationships between them + */ +export interface ProjectApplication { + id: string; + project_id: string; + application_id: string; + created_at: string; +} From 25c1e3d01d991b2c08625b991ce33dc8e271b714 Mon Sep 17 00:00:00 2001 From: Adrian Escutia Soto Date: Mon, 17 Mar 2025 07:01:20 -0500 Subject: [PATCH 04/50] chore: update @tanstack/react-query to version 5.68.0 --- package-lock.json | 113 +- package.json | 2 +- .../projects/detail/DraggableResourceList.tsx | 2 +- tables.sql | 1053 ----------------- 4 files changed, 11 insertions(+), 1159 deletions(-) delete mode 100644 tables.sql diff --git a/package-lock.json b/package-lock.json index 509f723..7ed155a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", "@supabase/supabase-js": "^2.49.1", - "@tanstack/react-query": "^5.56.2", + "@tanstack/react-query": "^5.68.0", "caniuse-lite": "^1.0.30001703", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -88,7 +88,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -871,7 +870,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -889,7 +887,6 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -904,7 +901,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -914,7 +910,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -924,14 +919,12 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -977,7 +970,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -2883,9 +2875,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.67.3", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.67.3.tgz", - "integrity": "sha512-pq76ObpjcaspAW4OmCbpXLF6BCZP2Zr/J5ztnyizXhSlNe7fIUp0QKZsd0JMkw9aDa+vxDX/OY7N+hjNY/dCGg==", + "version": "5.68.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.68.0.tgz", + "integrity": "sha512-r8rFYYo8/sY/LNaOqX84h12w7EQev4abFXDWy4UoDVUJzJ5d9Fbmb8ayTi7ScG+V0ap44SF3vNs/45mkzDGyGw==", "license": "MIT", "funding": { "type": "github", @@ -2893,12 +2885,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.67.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.67.3.tgz", - "integrity": "sha512-u/n2HsQeH1vpZIOzB/w2lqKlXUDUKo6BxTdGXSMvNzIq5MHYFckRMVuFABp+QB7RN8LFXWV6X1/oSkuDq+MPIA==", + "version": "5.68.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.68.0.tgz", + "integrity": "sha512-mMOdGDKlwTP/WV72QqSNf4PAMeoBp/DqBHQ222wBfb51Looi8QUqnCnb9O98ZgvNISmy6fzxRGBJdZ+9IBvX2Q==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.67.3" + "@tanstack/query-core": "5.68.0" }, "funding": { "type": "github", @@ -3043,7 +3035,7 @@ "version": "18.3.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -3340,7 +3332,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3353,7 +3344,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3369,14 +3359,12 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -3390,7 +3378,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -3469,14 +3456,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3555,7 +3540,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -3602,7 +3586,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -3627,7 +3610,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -3677,7 +3659,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3690,7 +3671,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/commander": { @@ -3728,7 +3708,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3752,7 +3731,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -3939,7 +3917,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, "license": "Apache-2.0" }, "node_modules/dir-glob": { @@ -3958,7 +3935,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, "license": "MIT" }, "node_modules/dom-helpers": { @@ -3975,7 +3951,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -4023,7 +3998,6 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/esbuild": { @@ -4461,7 +4435,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -4506,7 +4479,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4521,7 +4493,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4562,7 +4533,6 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -4583,7 +4553,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -4596,7 +4565,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4606,7 +4574,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -4678,7 +4645,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4761,7 +4727,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -4774,7 +4739,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -4799,7 +4763,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4830,14 +4793,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -4853,7 +4814,6 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -4939,7 +4899,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -4952,7 +4911,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -5032,7 +4990,6 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, "license": "ISC" }, "node_modules/lucide-react": { @@ -5114,7 +5071,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -5131,7 +5087,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -5143,7 +5098,6 @@ "version": "3.3.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz", "integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==", - "dev": true, "funding": [ { "type": "github", @@ -5186,7 +5140,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5215,7 +5168,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -5284,7 +5236,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/pako": { @@ -5319,7 +5270,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5329,14 +5279,12 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -5362,7 +5310,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -5381,7 +5328,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5391,7 +5337,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -5465,7 +5410,6 @@ "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -5494,7 +5438,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -5512,7 +5455,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -5532,7 +5474,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -5568,7 +5509,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -5594,7 +5534,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -5622,7 +5561,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { @@ -5926,7 +5864,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -5936,7 +5873,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -5996,7 +5932,6 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -6123,7 +6058,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -6136,7 +6070,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6146,7 +6079,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -6178,7 +6110,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -6188,7 +6119,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -6207,7 +6137,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6222,7 +6151,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6232,14 +6160,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6252,7 +6178,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -6269,7 +6194,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6282,7 +6206,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6326,7 +6249,6 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -6349,7 +6271,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -6372,7 +6293,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6395,7 +6315,6 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -6442,7 +6361,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -6456,7 +6374,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -6466,7 +6383,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -6537,7 +6453,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, "license": "Apache-2.0" }, "node_modules/tslib": { @@ -6723,7 +6638,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/vaul": { @@ -7271,7 +7185,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -7297,7 +7210,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -7316,7 +7228,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -7334,7 +7245,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7344,14 +7254,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -7366,7 +7274,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -7379,7 +7286,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7413,7 +7319,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 5164bac..152cd7a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", "@supabase/supabase-js": "^2.49.1", - "@tanstack/react-query": "^5.56.2", + "@tanstack/react-query": "^5.68.0", "caniuse-lite": "^1.0.30001703", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/components/projects/detail/DraggableResourceList.tsx b/src/components/projects/detail/DraggableResourceList.tsx index 007aae7..4ef901b 100644 --- a/src/components/projects/detail/DraggableResourceList.tsx +++ b/src/components/projects/detail/DraggableResourceList.tsx @@ -1,5 +1,5 @@ -import React from 'react'; +import * as React from 'react'; import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Application } from '@/types/application'; diff --git a/tables.sql b/tables.sql deleted file mode 100644 index e464e83..0000000 --- a/tables.sql +++ /dev/null @@ -1,1053 +0,0 @@ --- Create schemas if they don't exist -CREATE SCHEMA IF NOT EXISTS api; - --- Create tables for profiles if it doesn't exist yet -CREATE TABLE IF NOT EXISTS api.profiles ( - id UUID REFERENCES auth.users(id) PRIMARY KEY, - full_name TEXT, - bio TEXT, - job_title TEXT, - company TEXT, - plan_id TEXT NOT NULL DEFAULT 'free', - created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() -); - --- Create organizations table if it doesn't exist -CREATE TABLE IF NOT EXISTS api.organizations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - slug TEXT NOT NULL UNIQUE, - description TEXT, - logo_url TEXT, - is_global BOOLEAN DEFAULT FALSE, - is_public BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() -); - --- Create organization_members table if it doesn't exist -CREATE TABLE IF NOT EXISTS api.organization_members ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - organization_id UUID REFERENCES api.organizations(id) ON DELETE CASCADE, - user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, - role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member')), - created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), - UNIQUE (organization_id, user_id) -); - --- Create projects table if it doesn't exist -CREATE TABLE IF NOT EXISTS api.projects ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - description TEXT, - status TEXT DEFAULT 'active', - tags TEXT[] DEFAULT '{}', - favorite BOOLEAN DEFAULT FALSE, - user_id UUID REFERENCES auth.users(id), - organization_id UUID REFERENCES api.organizations(id) ON DELETE SET NULL, - is_public BOOLEAN DEFAULT FALSE, - applications_count INTEGER DEFAULT 0, - servers_count INTEGER DEFAULT 0, - tools_count INTEGER DEFAULT 0, - created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() -); - --- Create applications table if it doesn't exist -CREATE TABLE IF NOT EXISTS api.applications ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - description TEXT, - category TEXT, - status TEXT DEFAULT 'active', - tags TEXT[] DEFAULT '{}', - favorite BOOLEAN DEFAULT FALSE, - user_id UUID REFERENCES auth.users(id), - organization_id UUID REFERENCES api.organizations(id) ON DELETE SET NULL, - project_id UUID REFERENCES api.projects(id) ON DELETE SET NULL, - is_public BOOLEAN DEFAULT FALSE, - endpoints_count INTEGER DEFAULT 0, - tools_count INTEGER DEFAULT 0, - created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() -); - --- Create servers table if it doesn't exist -CREATE TABLE IF NOT EXISTS api.servers ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - description TEXT, - type TEXT, - status TEXT DEFAULT 'active', - tags TEXT[] DEFAULT '{}', - favorite BOOLEAN DEFAULT FALSE, - user_id UUID REFERENCES auth.users(id), - organization_id UUID REFERENCES api.organizations(id) ON DELETE SET NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() -); - --- Create ai_tools table -CREATE TABLE IF NOT EXISTS api.ai_tools ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - description TEXT, - category TEXT, - status TEXT DEFAULT 'active', - tags TEXT[] DEFAULT '{}', - favorite BOOLEAN DEFAULT FALSE, - user_id UUID REFERENCES auth.users(id), - organization_id UUID REFERENCES api.organizations(id) ON DELETE SET NULL, - agents_count INTEGER DEFAULT 0, - applications_count INTEGER DEFAULT 0, - servers_count INTEGER DEFAULT 0, - created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() -); - --- Create application_apis table -CREATE TABLE IF NOT EXISTS api.application_apis ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - application_id UUID REFERENCES api.applications(id) ON DELETE CASCADE, - name TEXT NOT NULL, - description TEXT, - source_uri TEXT, - source_content TEXT, - status TEXT DEFAULT 'active', - version TEXT, - protocol TEXT CHECK (protocol IN ('REST', 'gRPC', 'WebSockets', 'GraphQL')), - is_public BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() -); - --- Create application_services table -CREATE TABLE IF NOT EXISTS api.application_services ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - api_id UUID REFERENCES api.application_apis(id) ON DELETE CASCADE, - name TEXT NOT NULL, - description TEXT, - summary TEXT, - tags TEXT[] DEFAULT '{}', - path TEXT, - method TEXT, - created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() -); - --- Create application_service_messages table -CREATE TABLE IF NOT EXISTS api.application_service_messages ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - service_id UUID REFERENCES api.application_services(id) ON DELETE CASCADE, - name TEXT NOT NULL, - description TEXT, - message_type TEXT CHECK (message_type IN ('request', 'response')), - schema TEXT, - created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() -); - --- Add relationship between AI tools and application services -ALTER TABLE api.ai_tools -ADD COLUMN IF NOT EXISTS service_id UUID REFERENCES api.application_services(id) ON DELETE SET NULL; - --- Create a global organization if it doesn't exist -INSERT INTO api.organizations (name, slug, description, is_global, is_public) -SELECT 'Global', 'global', 'Public organization available to all users', TRUE, TRUE -WHERE NOT EXISTS (SELECT 1 FROM api.organizations WHERE is_global = TRUE); - --- Create trigger function to create user organization -CREATE OR REPLACE FUNCTION api.handle_new_user() -RETURNS TRIGGER AS $$ -DECLARE - org_id UUID; -BEGIN - -- Create profile - INSERT INTO api.profiles (id, full_name, plan_id) - VALUES (NEW.id, NEW.raw_user_meta_data->>'full_name', COALESCE(NEW.raw_user_meta_data->>'plan_id', 'free')); - - -- Create user's personal organization - INSERT INTO api.organizations (name, slug, description) - VALUES ( - COALESCE(NEW.raw_user_meta_data->>'full_name', 'User ' || NEW.id), - 'user-' || lower(replace(NEW.id::text, '-', '')), - 'Personal organization for ' || COALESCE(NEW.raw_user_meta_data->>'full_name', 'User ' || NEW.id) - ) - RETURNING id INTO org_id; - - -- Add user as owner of their organization - INSERT INTO api.organization_members (organization_id, user_id, role) - VALUES (org_id, NEW.id, 'owner'); - - RETURN NEW; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Ensure trigger is created -DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; -CREATE TRIGGER on_auth_user_created - AFTER INSERT ON auth.users - FOR EACH ROW EXECUTE FUNCTION api.handle_new_user(); - --- Create RPC functions for organization operations -CREATE OR REPLACE FUNCTION api.list_organizations() -RETURNS SETOF api.organizations AS $$ - SELECT * FROM api.organizations WHERE is_public = TRUE OR is_global = TRUE -$$ LANGUAGE SQL SECURITY DEFINER; - -CREATE OR REPLACE FUNCTION api.list_user_organizations(user_id UUID) -RETURNS TABLE ( - id UUID, - name TEXT, - slug TEXT, - description TEXT, - logo_url TEXT, - is_global BOOLEAN, - is_public BOOLEAN, - created_at TIMESTAMPTZ, - updated_at TIMESTAMPTZ, - role TEXT -) AS $$ - SELECT o.*, m.role - FROM api.organizations o - LEFT JOIN api.organization_members m ON o.id = m.organization_id AND m.user_id = list_user_organizations.user_id - WHERE o.is_global = TRUE OR m.user_id = list_user_organizations.user_id -$$ LANGUAGE SQL SECURITY DEFINER; - -CREATE OR REPLACE FUNCTION api.create_organization( - org_name TEXT, - org_slug TEXT, - org_description TEXT DEFAULT NULL, - org_logo_url TEXT DEFAULT NULL -) -RETURNS api.organizations AS $$ -DECLARE - new_org api.organizations; -BEGIN - INSERT INTO api.organizations (name, slug, description, logo_url) - VALUES (org_name, org_slug, org_description, org_logo_url) - RETURNING * INTO new_org; - - RETURN new_org; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -CREATE OR REPLACE FUNCTION api.add_organization_member( - org_id UUID, - member_id UUID, - member_role TEXT -) -RETURNS api.organization_members AS $$ -DECLARE - new_member api.organization_members; -BEGIN - INSERT INTO api.organization_members (organization_id, user_id, role) - VALUES (org_id, member_id, member_role) - RETURNING * INTO new_member; - - RETURN new_member; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -CREATE OR REPLACE FUNCTION api.add_member_by_email( - org_id UUID, - member_email TEXT, - member_role TEXT -) -RETURNS api.organization_members AS $$ -DECLARE - user_id UUID; - new_member api.organization_members; -BEGIN - -- Find user by email - SELECT id INTO user_id - FROM auth.users - WHERE email = member_email; - - IF user_id IS NULL THEN - RAISE EXCEPTION 'User with email % not found', member_email; - END IF; - - -- Add member to organization - INSERT INTO api.organization_members (organization_id, user_id, role) - VALUES (org_id, user_id, member_role) - RETURNING * INTO new_member; - - RETURN new_member; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -CREATE OR REPLACE FUNCTION api.list_organization_members(org_id UUID) -RETURNS SETOF api.organization_members AS $$ - SELECT * FROM api.organization_members WHERE organization_id = org_id -$$ LANGUAGE SQL SECURITY DEFINER; - --- Add RLS policies -ALTER TABLE api.organizations ENABLE ROW LEVEL SECURITY; -ALTER TABLE api.organization_members ENABLE ROW LEVEL SECURITY; -ALTER TABLE api.application_apis ENABLE ROW LEVEL SECURITY; -ALTER TABLE api.application_services ENABLE ROW LEVEL SECURITY; -ALTER TABLE api.application_service_messages ENABLE ROW LEVEL SECURITY; -ALTER TABLE api.servers ENABLE ROW LEVEL SECURITY; -ALTER TABLE api.ai_tools ENABLE ROW LEVEL SECURITY; -ALTER TABLE api.applications ENABLE ROW LEVEL SECURITY; -ALTER TABLE api.projects ENABLE ROW LEVEL SECURITY; -ALTER TABLE api.profiles ENABLE ROW LEVEL SECURITY; - --- Default access policies for profiles -CREATE POLICY "Users can view their own profile" ON api.profiles - FOR SELECT USING (auth.uid() = id); - -CREATE POLICY "Users can update their own profile" ON api.profiles - FOR UPDATE USING (auth.uid() = id); - --- Organizations access policies -CREATE POLICY "Anyone can view public or global organizations" ON api.organizations - FOR SELECT USING (is_public = TRUE OR is_global = TRUE); - -CREATE POLICY "Organization members can view their organizations" ON api.organizations - FOR SELECT USING ( - EXISTS (SELECT 1 FROM api.organization_members WHERE organization_id = id AND user_id = auth.uid()) - ); - -CREATE POLICY "Organization owners can update their organizations" ON api.organizations - FOR UPDATE USING ( - EXISTS (SELECT 1 FROM api.organization_members WHERE organization_id = id AND user_id = auth.uid() AND role = 'owner') - ); - -CREATE POLICY "Organization owners can delete their organizations" ON api.organizations - FOR DELETE USING ( - EXISTS (SELECT 1 FROM api.organization_members WHERE organization_id = id AND user_id = auth.uid() AND role = 'owner') - ); - -CREATE POLICY "Authenticated users can create organizations" ON api.organizations - FOR INSERT WITH CHECK (auth.role() = 'authenticated'); - --- Organization members access policies -CREATE POLICY "Organization members can view other members" ON api.organization_members - FOR SELECT USING ( - organization_id IN (SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid()) - ); - -CREATE POLICY "Organization admins and owners can create members" ON api.organization_members - FOR INSERT WITH CHECK ( - EXISTS ( - SELECT 1 FROM api.organization_members - WHERE organization_id = organization_id - AND user_id = auth.uid() - AND role IN ('admin', 'owner') - ) - ); - -CREATE POLICY "Organization owners can delete members" ON api.organization_members - FOR DELETE USING ( - EXISTS ( - SELECT 1 FROM api.organization_members - WHERE organization_id = organization_id - AND user_id = auth.uid() - AND role = 'owner' - ) - ); - --- Applications access policies -CREATE POLICY "Anyone can view public applications" ON api.applications - FOR SELECT USING ( - is_public = TRUE OR - organization_id IN ( - SELECT id FROM api.organizations WHERE is_global = TRUE OR is_public = TRUE - ) - ); - -CREATE POLICY "Organization members can view their applications" ON api.applications - FOR SELECT USING ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can create applications" ON api.applications - FOR INSERT WITH CHECK ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can update their applications" ON api.applications - FOR UPDATE USING ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can delete their applications" ON api.applications - FOR DELETE USING ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - --- Application APIs access policies -CREATE POLICY "Anyone can view public APIs" ON api.application_apis - FOR SELECT USING ( - is_public = TRUE OR - EXISTS ( - SELECT 1 FROM api.applications a - WHERE a.id = application_id AND ( - a.is_public = TRUE OR - a.organization_id IN (SELECT id FROM api.organizations WHERE is_global = TRUE OR is_public = TRUE) - ) - ) - ); - -CREATE POLICY "Organization members can view their APIs" ON api.application_apis - FOR SELECT USING ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organizations o ON a.organization_id = o.id - JOIN api.organization_members m ON o.id = m.organization_id - WHERE a.id = application_id AND m.user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can create APIs" ON api.application_apis - FOR INSERT WITH CHECK ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organizations o ON a.organization_id = o.id - JOIN api.organization_members m ON o.id = m.organization_id - WHERE a.id = application_id AND m.user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can update their APIs" ON api.application_apis - FOR UPDATE USING ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organizations o ON a.organization_id = o.id - JOIN api.organization_members m ON o.id = m.organization_id - WHERE a.id = application_id AND m.user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can delete their APIs" ON api.application_apis - FOR DELETE USING ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organizations o ON a.organization_id = o.id - JOIN api.organization_members m ON o.id = m.organization_id - WHERE a.id = application_id AND m.user_id = auth.uid() - ) - ); - --- Similar policies for application_services -CREATE POLICY "Anyone can view services of public APIs" ON api.application_services - FOR SELECT USING ( - EXISTS ( - SELECT 1 FROM api.application_apis a - WHERE a.id = api_id AND ( - a.is_public = TRUE OR - EXISTS ( - SELECT 1 FROM api.applications app - WHERE app.id = a.application_id AND ( - app.is_public = TRUE OR - app.organization_id IN (SELECT id FROM api.organizations WHERE is_global = TRUE OR is_public = TRUE) - ) - ) - ) - ) - ); - -CREATE POLICY "Organization members can view their services" ON api.application_services - FOR SELECT USING ( - EXISTS ( - SELECT 1 FROM api.application_apis a - JOIN api.applications app ON a.application_id = app.id - JOIN api.organizations o ON app.organization_id = o.id - JOIN api.organization_members m ON o.id = m.organization_id - WHERE a.id = api_id AND m.user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can create services" ON api.application_services - FOR INSERT WITH CHECK ( - EXISTS ( - SELECT 1 FROM api.application_apis a - JOIN api.applications app ON a.application_id = app.id - JOIN api.organizations o ON app.organization_id = o.id - JOIN api.organization_members m ON o.id = m.organization_id - WHERE a.id = api_id AND m.user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can update their services" ON api.application_services - FOR UPDATE USING ( - EXISTS ( - SELECT 1 FROM api.application_apis a - JOIN api.applications app ON a.application_id = app.id - JOIN api.organizations o ON app.organization_id = o.id - JOIN api.organization_members m ON o.id = m.organization_id - WHERE a.id = api_id AND m.user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can delete their services" ON api.application_services - FOR DELETE USING ( - EXISTS ( - SELECT 1 FROM api.application_apis a - JOIN api.applications app ON a.application_id = app.id - JOIN api.organizations o ON app.organization_id = o.id - JOIN api.organization_members m ON o.id = m.organization_id - WHERE a.id = api_id AND m.user_id = auth.uid() - ) - ); - --- Similar policies for application_service_messages -CREATE POLICY "Anyone can view messages of public services" ON api.application_service_messages - FOR SELECT USING ( - EXISTS ( - SELECT 1 FROM api.application_services s - JOIN api.application_apis a ON s.api_id = a.id - WHERE s.id = service_id AND ( - a.is_public = TRUE OR - EXISTS ( - SELECT 1 FROM api.applications app - WHERE app.id = a.application_id AND ( - app.is_public = TRUE OR - app.organization_id IN (SELECT id FROM api.organizations WHERE is_global = TRUE OR is_public = TRUE) - ) - ) - ) - ) - ); - -CREATE POLICY "Organization members can view their messages" ON api.application_service_messages - FOR SELECT USING ( - EXISTS ( - SELECT 1 FROM api.application_services s - JOIN api.application_apis a ON s.api_id = a.id - JOIN api.applications app ON a.application_id = app.id - JOIN api.organizations o ON app.organization_id = o.id - JOIN api.organization_members m ON o.id = m.organization_id - WHERE s.id = service_id AND m.user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can create messages" ON api.application_service_messages - FOR INSERT WITH CHECK ( - EXISTS ( - SELECT 1 FROM api.application_services s - JOIN api.application_apis a ON s.api_id = a.id - JOIN api.applications app ON a.application_id = app.id - JOIN api.organizations o ON app.organization_id = o.id - JOIN api.organization_members m ON o.id = m.organization_id - WHERE s.id = service_id AND m.user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can update their messages" ON api.application_service_messages - FOR UPDATE USING ( - EXISTS ( - SELECT 1 FROM api.application_services s - JOIN api.application_apis a ON s.api_id = a.id - JOIN api.applications app ON a.application_id = app.id - JOIN api.organizations o ON app.organization_id = o.id - JOIN api.organization_members m ON o.id = m.organization_id - WHERE s.id = service_id AND m.user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can delete their messages" ON api.application_service_messages - FOR DELETE USING ( - EXISTS ( - SELECT 1 FROM api.application_services s - JOIN api.application_apis a ON s.api_id = a.id - JOIN api.applications app ON a.application_id = app.id - JOIN api.organizations o ON app.organization_id = o.id - JOIN api.organization_members m ON o.id = m.organization_id - WHERE s.id = service_id AND m.user_id = auth.uid() - ) - ); - --- Projects access policies -CREATE POLICY "Anyone can view public projects" ON api.projects - FOR SELECT USING ( - is_public = TRUE OR - organization_id IN ( - SELECT id FROM api.organizations WHERE is_global = TRUE OR is_public = TRUE - ) - ); - -CREATE POLICY "Organization members can view their projects" ON api.projects - FOR SELECT USING ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can create projects" ON api.projects - FOR INSERT WITH CHECK ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can update their projects" ON api.projects - FOR UPDATE USING ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can delete their projects" ON api.projects - FOR DELETE USING ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - --- Servers access policies -CREATE POLICY "Anyone can view servers from public orgs" ON api.servers - FOR SELECT USING ( - organization_id IN ( - SELECT id FROM api.organizations WHERE is_global = TRUE OR is_public = TRUE - ) - ); - -CREATE POLICY "Organization members can view their servers" ON api.servers - FOR SELECT USING ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can create servers" ON api.servers - FOR INSERT WITH CHECK ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can update their servers" ON api.servers - FOR UPDATE USING ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can delete their servers" ON api.servers - FOR DELETE USING ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - --- AI Tools access policies -CREATE POLICY "Anyone can view AI tools from public orgs" ON api.ai_tools - FOR SELECT USING ( - organization_id IN ( - SELECT id FROM api.organizations WHERE is_global = TRUE OR is_public = TRUE - ) - ); - -CREATE POLICY "Organization members can view their AI tools" ON api.ai_tools - FOR SELECT USING ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can create AI tools" ON api.ai_tools - FOR INSERT WITH CHECK ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can update their AI tools" ON api.ai_tools - FOR UPDATE USING ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - -CREATE POLICY "Organization members can delete their AI tools" ON api.ai_tools - FOR DELETE USING ( - organization_id IN ( - SELECT organization_id FROM api.organization_members WHERE user_id = auth.uid() - ) - ); - ----- --- Create function to handle user deletion -CREATE OR REPLACE FUNCTION api.handle_user_deletion() -RETURNS TRIGGER AS $$ -DECLARE - owned_orgs UUID[]; -BEGIN - -- Find organizations where this user is the only owner - SELECT ARRAY_AGG(organization_id) INTO owned_orgs - FROM ( - SELECT om.organization_id - FROM api.organization_members om - WHERE om.role = 'owner' - GROUP BY om.organization_id - HAVING COUNT(CASE WHEN om.role = 'owner' THEN 1 ELSE NULL END) = 1 - AND bool_or(om.user_id = OLD.id AND om.role = 'owner') - ) AS sole_owner_orgs; - - -- Delete organizations where this user is the only owner - IF array_length(owned_orgs, 1) > 0 THEN - DELETE FROM api.organizations - WHERE id = ANY(owned_orgs); - END IF; - - -- Remove user from all other organizations - DELETE FROM api.organization_members - WHERE user_id = OLD.id; - - RETURN OLD; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Create trigger for user deletion -DROP TRIGGER IF EXISTS on_auth_user_deleted ON auth.users; -CREATE TRIGGER on_auth_user_deleted - BEFORE DELETE ON auth.users - FOR EACH ROW EXECUTE FUNCTION api.handle_user_deletion(); - --- Create function to handle organization deletion (only owners can delete via RLS) -CREATE OR REPLACE FUNCTION api.handle_organization_deletion() -RETURNS TRIGGER AS $$ -BEGIN - -- Remove all members from the organization - DELETE FROM api.organization_members - WHERE organization_id = OLD.id; - - RETURN OLD; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Create trigger for organization deletion -DROP TRIGGER IF EXISTS on_organization_deleted ON api.organizations; -CREATE TRIGGER on_organization_deleted - BEFORE DELETE ON api.organizations - FOR EACH ROW EXECUTE FUNCTION api.handle_organization_deletion(); - --- Add RLS policy to allow only owners to delete organizations -DROP POLICY IF EXISTS "Organization owners can delete organizations" ON api.organizations; -CREATE POLICY "Organization owners can delete organizations" ON api.organizations - FOR DELETE USING ( - EXISTS ( - SELECT 1 FROM api.organization_members - WHERE organization_id = id - AND user_id = auth.uid() - AND role = 'owner' - ) - AND NOT is_global -- Prevent deletion of global organization - ); - ----- - --- Create tables for application APIs, services, and messages - --- Application APIs table -CREATE TABLE IF NOT EXISTS api.application_apis ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - description TEXT, - application_id UUID NOT NULL, - status TEXT DEFAULT 'active', - version TEXT, - endpoint_url TEXT, - documentation_url TEXT, - tags TEXT[] DEFAULT '{}', - created_at TIMESTAMPTZ DEFAULT now(), - updated_at TIMESTAMPTZ DEFAULT now() -); - --- Application Services table -CREATE TABLE IF NOT EXISTS api.application_services ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - description TEXT, - application_id UUID NOT NULL, - status TEXT DEFAULT 'active', - service_type TEXT, - tags TEXT[] DEFAULT '{}', - created_at TIMESTAMPTZ DEFAULT now(), - updated_at TIMESTAMPTZ DEFAULT now() -); - --- Application Messages table -CREATE TABLE IF NOT EXISTS api.application_messages ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - title TEXT NOT NULL, - content TEXT NOT NULL, - application_id UUID NOT NULL, - message_type TEXT DEFAULT 'notification', - status TEXT DEFAULT 'unread', - created_at TIMESTAMPTZ DEFAULT now(), - updated_at TIMESTAMPTZ DEFAULT now() -); - --- Add relationship between AI Tools and Application Services -ALTER TABLE api.ai_tools ADD COLUMN IF NOT EXISTS application_service_id UUID; - --- Add relationship table between Servers and Applications (many-to-many) -CREATE TABLE IF NOT EXISTS api.server_applications ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - server_id UUID NOT NULL, - application_id UUID NOT NULL, - created_at TIMESTAMPTZ DEFAULT now(), - UNIQUE(server_id, application_id) -); - --- Add relationship table between Servers and AI Tools (many-to-many) -CREATE TABLE IF NOT EXISTS api.server_ai_tools ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - server_id UUID NOT NULL, - ai_tool_id UUID NOT NULL, - created_at TIMESTAMPTZ DEFAULT now(), - UNIQUE(server_id, ai_tool_id) -); - --- RLS policies for Application APIs -ALTER TABLE api.application_apis ENABLE ROW LEVEL SECURITY; - -CREATE POLICY "Users can view application APIs they have access to" ON api.application_apis -FOR SELECT USING ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organization_members om ON a.organization_id = om.organization_id - WHERE a.id = application_id - AND om.user_id = auth.uid() - ) - OR - EXISTS ( - SELECT 1 FROM api.applications a - WHERE a.id = application_id - AND a.organization_id IN (SELECT id FROM api.organizations WHERE is_public = TRUE) - ) -); - -CREATE POLICY "Users can insert application APIs they have access to" ON api.application_apis -FOR INSERT WITH CHECK ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organization_members om ON a.organization_id = om.organization_id - WHERE a.id = application_id - AND om.user_id = auth.uid() - AND om.role IN ('owner', 'admin') - ) -); - -CREATE POLICY "Users can update application APIs they have access to" ON api.application_apis -FOR UPDATE USING ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organization_members om ON a.organization_id = om.organization_id - WHERE a.id = application_id - AND om.user_id = auth.uid() - AND om.role IN ('owner', 'admin') - ) -); - -CREATE POLICY "Users can delete application APIs they have access to" ON api.application_apis -FOR DELETE USING ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organization_members om ON a.organization_id = om.organization_id - WHERE a.id = application_id - AND om.user_id = auth.uid() - AND om.role IN ('owner', 'admin') - ) -); - --- RLS policies for Application Services -ALTER TABLE api.application_services ENABLE ROW LEVEL SECURITY; - -CREATE POLICY "Users can view application services they have access to" ON api.application_services -FOR SELECT USING ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organization_members om ON a.organization_id = om.organization_id - WHERE a.id = application_id - AND om.user_id = auth.uid() - ) - OR - EXISTS ( - SELECT 1 FROM api.applications a - WHERE a.id = application_id - AND a.organization_id IN (SELECT id FROM api.organizations WHERE is_public = TRUE) - ) -); - -CREATE POLICY "Users can insert application services they have access to" ON api.application_services -FOR INSERT WITH CHECK ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organization_members om ON a.organization_id = om.organization_id - WHERE a.id = application_id - AND om.user_id = auth.uid() - AND om.role IN ('owner', 'admin') - ) -); - -CREATE POLICY "Users can update application services they have access to" ON api.application_services -FOR UPDATE USING ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organization_members om ON a.organization_id = om.organization_id - WHERE a.id = application_id - AND om.user_id = auth.uid() - AND om.role IN ('owner', 'admin') - ) -); - -CREATE POLICY "Users can delete application services they have access to" ON api.application_services -FOR DELETE USING ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organization_members om ON a.organization_id = om.organization_id - WHERE a.id = application_id - AND om.user_id = auth.uid() - AND om.role IN ('owner', 'admin') - ) -); - --- RLS policies for Application Messages -ALTER TABLE api.application_messages ENABLE ROW LEVEL SECURITY; - -CREATE POLICY "Users can view application messages they have access to" ON api.application_messages -FOR SELECT USING ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organization_members om ON a.organization_id = om.organization_id - WHERE a.id = application_id - AND om.user_id = auth.uid() - ) - OR - EXISTS ( - SELECT 1 FROM api.applications a - WHERE a.id = application_id - AND a.organization_id IN (SELECT id FROM api.organizations WHERE is_public = TRUE) - ) -); - -CREATE POLICY "Users can insert application messages they have access to" ON api.application_messages -FOR INSERT WITH CHECK ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organization_members om ON a.organization_id = om.organization_id - WHERE a.id = application_id - AND om.user_id = auth.uid() - AND om.role IN ('owner', 'admin', 'member') - ) -); - -CREATE POLICY "Users can update application messages they have access to" ON api.application_messages -FOR UPDATE USING ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organization_members om ON a.organization_id = om.organization_id - WHERE a.id = application_id - AND om.user_id = auth.uid() - AND om.role IN ('owner', 'admin') - ) -); - -CREATE POLICY "Users can delete application messages they have access to" ON api.application_messages -FOR DELETE USING ( - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organization_members om ON a.organization_id = om.organization_id - WHERE a.id = application_id - AND om.user_id = auth.uid() - AND om.role IN ('owner', 'admin') - ) -); - --- RLS policies for server-application relationships -ALTER TABLE api.server_applications ENABLE ROW LEVEL SECURITY; - -CREATE POLICY "Users can view server applications they have access to" ON api.server_applications -FOR SELECT USING ( - EXISTS ( - SELECT 1 FROM api.servers s - JOIN api.organization_members om ON s.organization_id = om.organization_id - WHERE s.id = server_id - AND om.user_id = auth.uid() - ) - OR - EXISTS ( - SELECT 1 FROM api.applications a - JOIN api.organization_members om ON a.organization_id = om.organization_id - WHERE a.id = application_id - AND om.user_id = auth.uid() - ) -); - -CREATE POLICY "Users can insert server applications they have access to" ON api.server_applications -FOR INSERT WITH CHECK ( - EXISTS ( - SELECT 1 FROM api.servers s - JOIN api.organization_members om ON s.organization_id = om.organization_id - WHERE s.id = server_id - AND om.user_id = auth.uid() - AND om.role IN ('owner', 'admin') - ) -); - -CREATE POLICY "Users can delete server applications they have access to" ON api.server_applications -FOR DELETE USING ( - EXISTS ( - SELECT 1 FROM api.servers s - JOIN api.organization_members om ON s.organization_id = om.organization_id - WHERE s.id = server_id - AND om.user_id = auth.uid() - AND om.role IN ('owner', 'admin') - ) -); - --- RLS policies for server-tools relationships -ALTER TABLE api.server_ai_tools ENABLE ROW LEVEL SECURITY; - -CREATE POLICY "Users can view server AI tools they have access to" ON api.server_ai_tools -FOR SELECT USING ( - EXISTS ( - SELECT 1 FROM api.servers s - JOIN api.organization_members om ON s.organization_id = om.organization_id - WHERE s.id = server_id - AND om.user_id = auth.uid() - ) - OR - EXISTS ( - SELECT 1 FROM api.ai_tools t - JOIN api.organization_members om ON t.organization_id = om.organization_id - WHERE t.id = ai_tool_id - AND om.user_id = auth.uid() - ) -); - -CREATE POLICY "Users can insert server AI tools they have access to" ON api.server_ai_tools -FOR INSERT WITH CHECK ( - EXISTS ( - SELECT 1 FROM api.servers s - JOIN api.organization_members om ON s.organization_id = om.organization_id - WHERE s.id = server_id - AND om.user_id = auth.uid() - AND om.role IN ('owner', 'admin') - ) -); - -CREATE POLICY "Users can delete server AI tools they have access to" ON api.server_ai_tools -FOR DELETE USING ( - EXISTS ( - SELECT 1 FROM api.servers s - JOIN api.organization_members om ON s.organization_id = om.organization_id - WHERE s.id = server_id - AND om.user_id = auth.uid() - AND om.role IN ('owner', 'admin') - ) -); From f38eb3f018868a917847ca53b7a1f532b1a44198 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 12:13:24 +0000 Subject: [PATCH 05/50] Run reviewed SQL queries --- src/hooks/useProjectTools.tsx | 60 ++++++++++++++++++++---------- src/integrations/supabase/types.ts | 46 ++++++++++++++++++----- src/types/project-tool.ts | 11 ++++++ 3 files changed, 88 insertions(+), 29 deletions(-) create mode 100644 src/types/project-tool.ts diff --git a/src/hooks/useProjectTools.tsx b/src/hooks/useProjectTools.tsx index 1adf24c..77fc013 100644 --- a/src/hooks/useProjectTools.tsx +++ b/src/hooks/useProjectTools.tsx @@ -3,15 +3,9 @@ import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { supabase } from '@/integrations/supabase/client'; import { AITool } from '@/types/ai-tool'; +import { ProjectTool } from '@/types/project-tool'; import { toast } from 'sonner'; -interface ProjectTool { - id: string; - project_id: string; - ai_tool_id: string; - created_at: string; -} - export function useProjectTools(projectId: string) { const queryClient = useQueryClient(); const [availableTools, setAvailableTools] = useState([]); @@ -30,22 +24,43 @@ export function useProjectTools(projectId: string) { }, }); - // Fetch AI tools associated with the project + // Fetch project_tools join records + const { data: projectToolsJoin, isLoading: isLoadingJoin } = useQuery({ + queryKey: ['project-tools-join', projectId], + queryFn: async () => { + const { data, error } = await supabase + .from('project_tools') + .select('*') + .eq('project_id', projectId); + + if (error) throw error; + return data as ProjectTool[]; + }, + enabled: !!projectId, + }); + + // Fetch AI tools associated with the project through the join table const { data: projectTools, isLoading: isLoadingAssociated } = useQuery({ queryKey: ['project-tools', projectId], queryFn: async () => { + if (!projectToolsJoin || projectToolsJoin.length === 0) { + return [] as AITool[]; + } + + const toolIds = projectToolsJoin.map(join => join.ai_tool_id); + const { data, error } = await supabase .from('ai_tools') .select('*') - .eq('project_id', projectId); + .in('id', toolIds); if (error) throw error; return data as AITool[]; }, - enabled: !!projectId, + enabled: !!projectId && !!projectToolsJoin, }); - // Associate/disassociate AI tool with project + // Associate/disassociate AI tool with project using the join table const { mutateAsync: updateToolAssociation } = useMutation({ mutationFn: async ({ toolId, @@ -55,24 +70,31 @@ export function useProjectTools(projectId: string) { action: 'associate' | 'disassociate' }) => { if (action === 'associate') { + // Add a record to the join table const { error } = await supabase - .from('ai_tools') - .update({ project_id: projectId }) - .eq('id', toolId); + .from('project_tools') + .insert({ + project_id: projectId, + ai_tool_id: toolId + }); if (error) throw error; } else { + // Remove the record from the join table const { error } = await supabase - .from('ai_tools') - .update({ project_id: null }) - .eq('id', toolId); + .from('project_tools') + .delete() + .match({ + project_id: projectId, + ai_tool_id: toolId + }); if (error) throw error; } }, onSuccess: () => { // Invalidate queries to refetch data - queryClient.invalidateQueries({ queryKey: ['ai-tools'] }); + queryClient.invalidateQueries({ queryKey: ['project-tools-join', projectId] }); queryClient.invalidateQueries({ queryKey: ['project-tools', projectId] }); }, onError: (error) => { @@ -117,7 +139,7 @@ export function useProjectTools(projectId: string) { return { availableTools, associatedTools, - isLoading: isLoadingAll || isLoadingAssociated, + isLoading: isLoadingAll || isLoadingAssociated || isLoadingJoin, handleMoveTool, }; } diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index ce5cf48..b6b3f26 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -280,7 +280,6 @@ export type Database = { is_public: boolean | null name: string organization_id: string | null - project_id: string | null slug: string status: string | null tags: string[] | null @@ -298,7 +297,6 @@ export type Database = { is_public?: boolean | null name: string organization_id?: string | null - project_id?: string | null slug: string status?: string | null tags?: string[] | null @@ -316,7 +314,6 @@ export type Database = { is_public?: boolean | null name?: string organization_id?: string | null - project_id?: string | null slug?: string status?: string | null tags?: string[] | null @@ -332,13 +329,6 @@ export type Database = { referencedRelation: "organizations" referencedColumns: ["id"] }, - { - foreignKeyName: "applications_project_id_fkey" - columns: ["project_id"] - isOneToOne: false - referencedRelation: "projects" - referencedColumns: ["id"] - }, ] } organization_members: { @@ -514,6 +504,42 @@ export type Database = { }, ] } + project_tools: { + Row: { + ai_tool_id: string + created_at: string + id: string + project_id: string + } + Insert: { + ai_tool_id: string + created_at?: string + id?: string + project_id: string + } + Update: { + ai_tool_id?: string + created_at?: string + id?: string + project_id?: string + } + Relationships: [ + { + foreignKeyName: "project_tools_ai_tool_id_fkey" + columns: ["ai_tool_id"] + isOneToOne: false + referencedRelation: "ai_tools" + referencedColumns: ["id"] + }, + { + foreignKeyName: "project_tools_project_id_fkey" + columns: ["project_id"] + isOneToOne: false + referencedRelation: "projects" + referencedColumns: ["id"] + }, + ] + } projects: { Row: { applications_count: number | null diff --git a/src/types/project-tool.ts b/src/types/project-tool.ts new file mode 100644 index 0000000..9c72b8e --- /dev/null +++ b/src/types/project-tool.ts @@ -0,0 +1,11 @@ + +/** + * Represents a join table record between projects and AI tools + * enabling many-to-many relationships between them + */ +export interface ProjectTool { + id: string; + project_id: string; + ai_tool_id: string; + created_at: string; +} From 17363051349528b9fa69ec80b13da63e7c219f98 Mon Sep 17 00:00:00 2001 From: Adrian Escutia Soto Date: Mon, 17 Mar 2025 18:47:18 -0500 Subject: [PATCH 06/50] chore: rename project to agentico-dashboard --- package.json | 2 +- src/utils/apiContentUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 152cd7a..bf524f3 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "vite_react_shadcn_ts", + "name": "agentico-dashboard", "private": true, "homepage": "https://app.agentico.dev/", "version": "0.1.0", diff --git a/src/utils/apiContentUtils.ts b/src/utils/apiContentUtils.ts index c2a2d56..599544e 100644 --- a/src/utils/apiContentUtils.ts +++ b/src/utils/apiContentUtils.ts @@ -1,4 +1,4 @@ -import pako from 'pako'; +import * as pako from 'pako'; /** * Convert a string to a compressed Uint8Array From 1935d40feca46553556db61b5a112a85ab41f650 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 00:45:53 +0000 Subject: [PATCH 07/50] Enhance API Source Section Implement dynamic behavior for API source URI and content based on user input. --- src/components/editor/CodeEditor.tsx | 39 ++++++--- src/integrations/supabase/types.ts | 6 +- src/pages/applications/api/ApiFormPage.tsx | 9 ++ .../applications/api/components/ApiForm.tsx | 14 ++- .../api/components/ApiSourceSection.tsx | 85 ++++++++++++++++++- 5 files changed, 133 insertions(+), 20 deletions(-) diff --git a/src/components/editor/CodeEditor.tsx b/src/components/editor/CodeEditor.tsx index 92b8d50..a51ca44 100644 --- a/src/components/editor/CodeEditor.tsx +++ b/src/components/editor/CodeEditor.tsx @@ -9,9 +9,10 @@ interface CodeEditorProps { onChange: (value: string) => void; language?: 'json' | 'yaml'; className?: string; + readOnly?: boolean; } -export function CodeEditor({ value, onChange, language = 'json', className }: CodeEditorProps) { +export function CodeEditor({ value, onChange, language = 'json', className, readOnly = false }: CodeEditorProps) { const [copied, setCopied] = useState(false); const [localValue, setLocalValue] = useState(value || ''); @@ -29,12 +30,16 @@ export function CodeEditor({ value, onChange, language = 'json', className }: Co }; const handleChange = (e: React.ChangeEvent) => { + if (readOnly) return; + const newValue = e.target.value; setLocalValue(newValue); onChange(newValue); }; const formatCode = () => { + if (readOnly) return; + try { if (language === 'json') { const formatted = JSON.stringify(JSON.parse(localValue || '{}'), null, 2); @@ -48,7 +53,7 @@ export function CodeEditor({ value, onChange, language = 'json', className }: Co }; return ( -
+
{language === 'json' ? ( @@ -57,18 +62,22 @@ export function CodeEditor({ value, onChange, language = 'json', className }: Co )} {language.toUpperCase()} + {readOnly && (Read Only)}
- + {!readOnly && ( + + )}
); diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index b6b3f26..677c75a 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -91,6 +91,7 @@ export type Database = { is_public: boolean | null name: string protocol: string | null + slug: string | null source_content: string | null source_uri: string | null status: string | null @@ -107,6 +108,7 @@ export type Database = { is_public?: boolean | null name: string protocol?: string | null + slug?: string | null source_content?: string | null source_uri?: string | null status?: string | null @@ -123,6 +125,7 @@ export type Database = { is_public?: boolean | null name?: string protocol?: string | null + slug?: string | null source_content?: string | null source_uri?: string | null status?: string | null @@ -406,7 +409,6 @@ export type Database = { description: string | null features: Json id: string - interval: "monthly" | "quarterly" | "yearly" name: string price: number updated_at: string @@ -417,7 +419,6 @@ export type Database = { description?: string | null features?: Json id?: string - interval: "monthly" | "quarterly" | "yearly" name: string price: number updated_at?: string @@ -428,7 +429,6 @@ export type Database = { description?: string | null features?: Json id?: string - interval?: "monthly" | "quarterly" | "yearly" name?: string price?: number updated_at?: string diff --git a/src/pages/applications/api/ApiFormPage.tsx b/src/pages/applications/api/ApiFormPage.tsx index e7298b8..ceb4cb7 100644 --- a/src/pages/applications/api/ApiFormPage.tsx +++ b/src/pages/applications/api/ApiFormPage.tsx @@ -140,6 +140,11 @@ export default function ApiFormPage() { return
Application ID is required
; } + // Generate a slug for the API if we have a name + const apiSlug = api?.name?.toLowerCase().replace(/[^a-z0-9]/g, '-') || + form.watch('name')?.toLowerCase().replace(/[^a-z0-9]/g, '-') || + 'api'; + // Prepare breadcrumb items const breadcrumbItems = [ { label: 'Applications', path: '/applications' }, @@ -186,6 +191,10 @@ export default function ApiFormPage() { onFetchContent={handleFetchContent} setShouldFetchContent={setShouldFetchContent} shouldFetchContent={shouldFetchContent} + applicationSlug={application?.slug} + organizationSlug={application?.organization_slug} + apiVersion={form.watch('version') || '1.0.0'} + apiSlug={apiSlug} /> diff --git a/src/pages/applications/api/components/ApiForm.tsx b/src/pages/applications/api/components/ApiForm.tsx index 5ebec6a..d6a9ad7 100644 --- a/src/pages/applications/api/components/ApiForm.tsx +++ b/src/pages/applications/api/components/ApiForm.tsx @@ -23,6 +23,10 @@ interface ApiFormProps { onFetchContent?: () => void; shouldFetchContent?: boolean; setShouldFetchContent?: (value: boolean) => void; + applicationSlug?: string; + organizationSlug?: string; + apiVersion?: string; + apiSlug?: string; } export function ApiForm({ @@ -37,7 +41,11 @@ export function ApiForm({ setCodeLanguage, onFetchContent, shouldFetchContent, - setShouldFetchContent + setShouldFetchContent, + applicationSlug, + organizationSlug, + apiVersion, + apiSlug }: ApiFormProps) { return (
@@ -148,6 +156,10 @@ export function ApiForm({ onFetchContent={onFetchContent} shouldFetchContent={shouldFetchContent} setShouldFetchContent={setShouldFetchContent} + applicationSlug={applicationSlug} + organizationSlug={organizationSlug} + apiVersion={apiVersion} + apiSlug={apiSlug} />
diff --git a/src/pages/applications/api/components/ApiSourceSection.tsx b/src/pages/applications/api/components/ApiSourceSection.tsx index d9f73f2..3c355c2 100644 --- a/src/pages/applications/api/components/ApiSourceSection.tsx +++ b/src/pages/applications/api/components/ApiSourceSection.tsx @@ -8,8 +8,9 @@ import { CodeEditor } from '@/components/editor/CodeEditor'; import { UseFormReturn } from 'react-hook-form'; import { ApplicationAPI } from '@/types/application'; import { Button } from '@/components/ui/button'; -import { Download, RefreshCw } from 'lucide-react'; +import { AlertCircle, Download, RefreshCw } from 'lucide-react'; import { Checkbox } from '@/components/ui/checkbox'; +import { Alert, AlertDescription } from '@/components/ui/alert'; interface ApiSourceSectionProps { form: UseFormReturn & { fetchContent?: boolean }>; @@ -20,6 +21,10 @@ interface ApiSourceSectionProps { onFetchContent?: () => void; shouldFetchContent?: boolean; setShouldFetchContent?: (value: boolean) => void; + applicationSlug?: string; + organizationSlug?: string; + apiVersion?: string; + apiSlug?: string; } export const ApiSourceSection: React.FC = ({ @@ -30,8 +35,30 @@ export const ApiSourceSection: React.FC = ({ setCodeLanguage, onFetchContent, shouldFetchContent, - setShouldFetchContent + setShouldFetchContent, + applicationSlug, + organizationSlug = 'global', + apiVersion = '1.0.0', + apiSlug }) => { + // Generate URN when source type changes to 'content' + React.useEffect(() => { + if (sourceType === 'content' && !form.getValues('source_content')) { + // If switching to content mode but no content yet, don't generate URN + return; + } + + if (sourceType === 'content') { + const name = form.getValues('name') || ''; + // Use apiSlug if provided, otherwise generate from name + const slug = apiSlug || name.toLowerCase().replace(/[^a-z0-9]/g, '-'); + // Generate the URN + const urn = `urn:agentico:apis:${organizationSlug}:${applicationSlug || 'app'}:${slug}:${apiVersion}`; + // Set the URN as source_uri + form.setValue('source_uri', urn); + } + }, [sourceType, form, applicationSlug, organizationSlug, apiVersion, apiSlug]); + return (

API Source

@@ -61,7 +88,11 @@ export const ApiSourceSection: React.FC = ({ Source URI
- + {onFetchContent && (
)} + + ( + + Source Content + {!field.value && ( + + + + The content is empty. Click the "Fetch" button above to load content from the URI, or check "Fetch content from URI when saving". + + + )} + + + + + API specification content (read-only in URI mode) + + + + )} + />
) : (
+ ( + + Generated URI + + + + + Automatically generated Agentico URN for this API specification + + + + )} + /> +
Format - + + + + + + + + +

Please enter a valid URL (https://example.com/spec.json)

+
+
+
{onFetchContent && ( @@ -135,7 +171,8 @@ export const ApiSourceSection: React.FC = ({ render={({ field }) => ( Source Content - {!field.value && ( + {!field.value && isUriMode() && ( + // color yellow @@ -149,7 +186,7 @@ export const ApiSourceSection: React.FC = ({ onChange={field.onChange} language={codeLanguage} className="min-h-[300px]" - readOnly={true} + readOnly={isUriMode()} /> @@ -172,7 +209,7 @@ export const ApiSourceSection: React.FC = ({ - Automatically generated Agentico URN for this API specification + Agentico URN for this API specification (manual editing) From aae94943062f8da8f5f3b57a03c99726badbca6c Mon Sep 17 00:00:00 2001 From: Adrian Escutia Soto Date: Tue, 18 Mar 2025 11:04:04 -0500 Subject: [PATCH 10/50] chore: bump vite version to fix CWE-346 --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ed155a..dd8b877 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "vite_react_shadcn_ts", + "name": "agentico-dashboard", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "vite_react_shadcn_ts", + "name": "agentico-dashboard", "version": "0.1.0", "dependencies": { "@hello-pangea/dnd": "^16.6.0", @@ -81,7 +81,7 @@ "tailwindcss": "^3.4.11", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", - "vite": ">=5.4.12" + "vite": "^5.4.14" } }, "node_modules/@alloc/quick-lru": { diff --git a/package.json b/package.json index bf524f3..5c8757b 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,6 @@ "tailwindcss": "^3.4.11", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", - "vite": ">=5.4.12" + "vite": "^5.4.14" } } From 230723f77430c590c190fdef96ef9a0cb99896f3 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:15:12 +0000 Subject: [PATCH 11/50] Fix API content decompression error Troubleshoots and fixes an issue where API content from Supabase fails to decompress due to an "unknown compression method" error. Ensures proper handling of compressed data. --- src/hooks/useApplicationApis.tsx | 27 ++++++-- .../api/components/ApiSourceSection.tsx | 17 ++--- src/utils/apiContentUtils.ts | 62 +++++++++++++++---- 3 files changed, 81 insertions(+), 25 deletions(-) diff --git a/src/hooks/useApplicationApis.tsx b/src/hooks/useApplicationApis.tsx index 86e8ddb..8256e3c 100644 --- a/src/hooks/useApplicationApis.tsx +++ b/src/hooks/useApplicationApis.tsx @@ -1,4 +1,3 @@ - import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { supabase } from '@/integrations/supabase/client'; import { useAuth } from './useAuth'; @@ -31,11 +30,21 @@ export function useApplicationApis(applicationId?: string) { // The source_content comes as base64 from Supabase when it's bytea if (api.source_content) { try { + console.log('Processing API content for:', api.name); const compressedData = base64ToUint8Array(api.source_content); api.source_content = decompressContent(compressedData); + console.log('Successfully decompressed content:', api.source_content.substring(0, 100) + '...'); } catch (err) { - console.error('Error decompressing API content:', err); - api.source_content = ''; // Reset if decompression fails + console.error('Error processing API content for:', api.name, err); + // Try to handle raw content if decompression fails + try { + // Attempt to just decode the base64 string directly + api.source_content = atob(api.source_content); + console.log('Fallback to direct base64 decode successful'); + } catch (decodeErr) { + console.error('Fallback decode also failed:', decodeErr); + api.source_content = ''; // Reset if all decompression fails + } } } return api as ApplicationAPI; @@ -270,11 +279,21 @@ export function useApplicationApi(id?: string) { // Process the binary data if (data.source_content) { try { + console.log('Processing single API content for:', data.name); const compressedData = base64ToUint8Array(data.source_content); data.source_content = decompressContent(compressedData); + console.log('Successfully decompressed content:', data.source_content.substring(0, 100) + '...'); } catch (err) { console.error('Error decompressing API content:', err); - data.source_content = ''; // Reset if decompression fails + // Try to handle raw content if decompression fails + try { + // Attempt to just decode the base64 string directly + data.source_content = atob(data.source_content); + console.log('Fallback to direct base64 decode successful'); + } catch (decodeErr) { + console.error('Fallback decode also failed:', decodeErr); + data.source_content = ''; // Reset if all decompression fails + } } } diff --git a/src/pages/applications/api/components/ApiSourceSection.tsx b/src/pages/applications/api/components/ApiSourceSection.tsx index 9ef226e..e41ed64 100644 --- a/src/pages/applications/api/components/ApiSourceSection.tsx +++ b/src/pages/applications/api/components/ApiSourceSection.tsx @@ -1,3 +1,4 @@ + import * as React from 'react'; import { FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; @@ -85,14 +86,16 @@ export const ApiSourceSection: React.FC = ({ function handleSourceTypeChange(value: 'uri' | 'content') { setSourceType(value); } + function isUriMode() { const uriValue = form.getValues('source_uri'); - return form.getValues('source_uri') && uriValue && !uriValue.startsWith('urn:') ? true : false; + return uriValue && !uriValue.startsWith('urn:') ? true : false; } + function isContentMode() { const isContent = form.getValues('source_content') && form.getValues('source_content').length > 0 ? true : false; const uriValue = form.getValues('source_uri'); - if (isContent && !uriValue && !uriValue.startsWith('urn:')) { + if (isContent && (!uriValue || !uriValue.startsWith('urn:'))) { generateURN(form, apiSlug, organizationSlug, applicationSlug, apiVersion); } return isContent; @@ -102,7 +105,6 @@ export const ApiSourceSection: React.FC = ({

API Source

- {/* onValueChange={(value) => setSourceType(value as 'uri' | 'content')} */} {sourceType === 'uri' ? (
= ({ Source Content {!field.value && isUriMode() && ( - // color yellow - - - + + + The content is empty. Click the "Fetch" button above to load content from the URI, or check "Fetch content from URI when saving". @@ -209,7 +210,7 @@ export const ApiSourceSection: React.FC = ({ - Agentico URN for this API specification (manual editing) + Agentico URN for this API specification (auto-generated) diff --git a/src/utils/apiContentUtils.ts b/src/utils/apiContentUtils.ts index 599544e..0f1dc49 100644 --- a/src/utils/apiContentUtils.ts +++ b/src/utils/apiContentUtils.ts @@ -1,3 +1,4 @@ + import * as pako from 'pako'; /** @@ -18,8 +19,33 @@ export const compressContent = (content: string): Uint8Array => { */ export const decompressContent = (compressedData: Uint8Array): string => { try { - const decompressed = pako.inflate(compressedData); - return new TextDecoder().decode(decompressed); + // First try with pako.inflate (for deflate compression) + try { + const decompressed = pako.inflate(compressedData); + return new TextDecoder().decode(decompressed); + } catch (inflateError) { + console.error('Error inflating content:', inflateError); + + // Fall back to try uncompressed data + try { + return new TextDecoder().decode(compressedData); + } catch (decodeError) { + console.error('Error decoding as uncompressed:', decodeError); + + // As a last resort, try UTF-8 decoding directly + let result = ''; + for (let i = 0; i < compressedData.length; i++) { + result += String.fromCharCode(compressedData[i]); + } + + // Check if the result looks like valid text + if (/^[\x20-\x7E\s]*$/.test(result)) { + return result; + } + + throw new Error('Could not decompress content with any method'); + } + } } catch (error) { console.error('Error decompressing content:', error); throw new Error('Failed to decompress content'); @@ -32,13 +58,18 @@ export const decompressContent = (compressedData: Uint8Array): string => { * @returns The Uint8Array */ export function base64ToUint8Array(base64: string): Uint8Array { - const binaryString = atob(base64); - const len = binaryString.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); + try { + const binaryString = atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } catch (error) { + console.error('Error converting base64 to Uint8Array:', error); + throw new Error('Failed to convert base64 to binary data'); } - return bytes; } /** @@ -47,12 +78,17 @@ export function base64ToUint8Array(base64: string): Uint8Array { * @returns The base64 string */ export function uint8ArrayToBase64(uint8Array: Uint8Array): string { - let binary = ''; - const len = uint8Array.byteLength; - for (let i = 0; i < len; i++) { - binary += String.fromCharCode(uint8Array[i]); + try { + let binary = ''; + const len = uint8Array.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(uint8Array[i]); + } + return btoa(binary); + } catch (error) { + console.error('Error converting Uint8Array to base64:', error); + throw new Error('Failed to convert binary data to base64'); } - return btoa(binary); } /** From 38514c94254bc9a94a8c350b821e3c007053c9b2 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:19:38 +0000 Subject: [PATCH 12/50] Refactor ApiSourceSection component Refactor ApiSourceSection into smaller, more manageable files for better organization and maintainability. --- .../applications/api/components/ApiForm.tsx | 2 +- .../api/components/ApiSourceSection.tsx | 261 ------------------ .../api-source/ApiSourceSection.tsx | 103 +++++++ .../api-source/ContentSourceSection.tsx | 79 ++++++ .../api-source/UriSourceSection.tsx | 129 +++++++++ .../api/components/api-source/index.ts | 2 + .../api/components/api-source/utils.ts | 23 ++ 7 files changed, 337 insertions(+), 262 deletions(-) delete mode 100644 src/pages/applications/api/components/ApiSourceSection.tsx create mode 100644 src/pages/applications/api/components/api-source/ApiSourceSection.tsx create mode 100644 src/pages/applications/api/components/api-source/ContentSourceSection.tsx create mode 100644 src/pages/applications/api/components/api-source/UriSourceSection.tsx create mode 100644 src/pages/applications/api/components/api-source/index.ts create mode 100644 src/pages/applications/api/components/api-source/utils.ts diff --git a/src/pages/applications/api/components/ApiForm.tsx b/src/pages/applications/api/components/ApiForm.tsx index d6a9ad7..61e0870 100644 --- a/src/pages/applications/api/components/ApiForm.tsx +++ b/src/pages/applications/api/components/ApiForm.tsx @@ -7,7 +7,7 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { ApplicationAPI } from '@/types/application'; -import { ApiSourceSection } from './ApiSourceSection'; +import { ApiSourceSection } from './api-source'; import TagsSelector from '@/components/applications/TagSelector'; interface ApiFormProps { diff --git a/src/pages/applications/api/components/ApiSourceSection.tsx b/src/pages/applications/api/components/ApiSourceSection.tsx deleted file mode 100644 index e41ed64..0000000 --- a/src/pages/applications/api/components/ApiSourceSection.tsx +++ /dev/null @@ -1,261 +0,0 @@ - -import * as React from 'react'; -import { FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { CodeEditor } from '@/components/editor/CodeEditor'; -import { UseFormReturn } from 'react-hook-form'; -import { ApplicationAPI } from '@/types/application'; -import { Button } from '@/components/ui/button'; -import { AlertCircle, Download, RefreshCw } from 'lucide-react'; -import { Checkbox } from '@/components/ui/checkbox'; -import { Alert, AlertDescription } from '@/components/ui/alert'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; - -interface ApiSourceSectionProps { - form: UseFormReturn & { fetchContent?: boolean }>; - sourceType: 'uri' | 'content'; - setSourceType: (type: 'uri' | 'content') => void; - codeLanguage: 'json' | 'yaml'; - setCodeLanguage: (language: 'json' | 'yaml') => void; - onFetchContent?: () => void; - shouldFetchContent?: boolean; - setShouldFetchContent?: (value: boolean) => void; - applicationSlug?: string; - organizationSlug?: string; - apiVersion?: string; - apiSlug?: string; -} - -export const ApiSourceSection: React.FC = ({ - form, - sourceType, - setSourceType, - codeLanguage, - setCodeLanguage, - onFetchContent, - shouldFetchContent, - setShouldFetchContent, - applicationSlug, - organizationSlug = 'global', - apiVersion = '1.0.0', - apiSlug -}) => { - // Add state for URI validation - const [isUriValid, setIsUriValid] = React.useState(true); - - // Generate URN when source type changes to 'content' - React.useEffect(() => { - if (sourceType === 'content' && !form.getValues('source_content')) { - // If switching to content mode but no content yet, don't generate URN - return; - } - }, [sourceType, form, applicationSlug, organizationSlug, apiVersion, apiSlug]); - - // Function to validate URI - function isValidUri(uri: string): boolean { - if (!uri) return true; // Empty URI is considered valid (not filled yet) - try { - new URL(uri); - return true; - } catch (e) { - // Allow URNs (they're valid internally) - return uri.startsWith('urn:'); - } - } - - // Validate URI when it changes - React.useEffect(() => { - if (sourceType === 'uri') { - const currentUri = form.getValues('source_uri'); - setIsUriValid(isValidUri(currentUri)); - } - }, [form.watch('source_uri'), sourceType]); - - function generateURN(form: UseFormReturn & { fetchContent?: boolean; }>, apiSlug: string, organizationSlug: string, applicationSlug: string, apiVersion: string) { - const name = form.getValues('name') || ''; - // Use apiSlug if provided, otherwise generate from name - const slug = apiSlug || name.toLowerCase().replace(/[^a-z0-9]/g, '-'); - // Generate the URN - const urn = `urn:agentico:apis:${organizationSlug}:${applicationSlug || 'app'}:${slug}:${apiVersion}`; - console.log('Generated URN:', urn); - // Set the URN as source_uri - form.setValue('source_uri', urn); - } - - function handleSourceTypeChange(value: 'uri' | 'content') { - setSourceType(value); - } - - function isUriMode() { - const uriValue = form.getValues('source_uri'); - return uriValue && !uriValue.startsWith('urn:') ? true : false; - } - - function isContentMode() { - const isContent = form.getValues('source_content') && form.getValues('source_content').length > 0 ? true : false; - const uriValue = form.getValues('source_uri'); - if (isContent && (!uriValue || !uriValue.startsWith('urn:'))) { - generateURN(form, apiSlug, organizationSlug, applicationSlug, apiVersion); - } - return isContent; - } - - return ( -
-

API Source

- - {sourceType === 'uri' ? ( -
- ( - - Source URI -
- - - - - - - - -

Please enter a valid URL (https://example.com/spec.json)

-
-
-
- {onFetchContent && ( - - )} -
- - Link to the API specification file (OpenAPI, Swagger, etc.) - - -
- )} - /> - - {setShouldFetchContent && ( -
- setShouldFetchContent(checked as boolean)} - /> - -
- )} - - ( - - Source Content - {!field.value && isUriMode() && ( - - - - The content is empty. Click the "Fetch" button above to load content from the URI, or check "Fetch content from URI when saving". - - - )} - - - - - API specification content (read-only in URI mode) - - - - )} - /> -
- ) : ( -
- ( - - Generated URI - - - - - Agentico URN for this API specification (auto-generated) - - - - )} - /> - -
- Format - -
- - ( - - Source Content - - - - - Paste or write your API specification (OpenAPI, Swagger, etc.) - - - - )} - /> -
- )} -
- ); -}; diff --git a/src/pages/applications/api/components/api-source/ApiSourceSection.tsx b/src/pages/applications/api/components/api-source/ApiSourceSection.tsx new file mode 100644 index 0000000..666b2cc --- /dev/null +++ b/src/pages/applications/api/components/api-source/ApiSourceSection.tsx @@ -0,0 +1,103 @@ + +import * as React from 'react'; +import { UseFormReturn } from 'react-hook-form'; +import { ApplicationAPI } from '@/types/application'; +import { UriSourceSection } from './UriSourceSection'; +import { ContentSourceSection } from './ContentSourceSection'; +import { generateURN } from './utils'; + +interface ApiSourceSectionProps { + form: UseFormReturn & { fetchContent?: boolean }>; + sourceType: 'uri' | 'content'; + setSourceType: (type: 'uri' | 'content') => void; + codeLanguage: 'json' | 'yaml'; + setCodeLanguage: (language: 'json' | 'yaml') => void; + onFetchContent?: () => void; + shouldFetchContent?: boolean; + setShouldFetchContent?: (value: boolean) => void; + applicationSlug?: string; + organizationSlug?: string; + apiVersion?: string; + apiSlug?: string; +} + +export const ApiSourceSection: React.FC = ({ + form, + sourceType, + setSourceType, + codeLanguage, + setCodeLanguage, + onFetchContent, + shouldFetchContent, + setShouldFetchContent, + applicationSlug, + organizationSlug = 'global', + apiVersion = '1.0.0', + apiSlug +}) => { + const [isUriValid, setIsUriValid] = React.useState(true); + + // Function to validate URI + function isValidUri(uri: string): boolean { + if (!uri) return true; // Empty URI is considered valid (not filled yet) + try { + new URL(uri); + return true; + } catch (e) { + // Allow URNs (they're valid internally) + return uri.startsWith('urn:'); + } + } + + // Validate URI when it changes + React.useEffect(() => { + if (sourceType === 'uri') { + const currentUri = form.getValues('source_uri'); + setIsUriValid(isValidUri(currentUri)); + } + }, [form.watch('source_uri'), sourceType]); + + // Handle source type changes + function handleSourceTypeChange(value: 'uri' | 'content') { + setSourceType(value); + } + + function isUriMode() { + const uriValue = form.getValues('source_uri'); + return uriValue && !uriValue.startsWith('urn:') ? true : false; + } + + function isContentMode() { + const isContent = form.getValues('source_content') && form.getValues('source_content').length > 0 ? true : false; + const uriValue = form.getValues('source_uri'); + if (isContent && (!uriValue || !uriValue.startsWith('urn:'))) { + generateURN(form, apiSlug, organizationSlug, applicationSlug, apiVersion); + } + return isContent; + } + + return ( +
+

API Source

+ + {sourceType === 'uri' ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/src/pages/applications/api/components/api-source/ContentSourceSection.tsx b/src/pages/applications/api/components/api-source/ContentSourceSection.tsx new file mode 100644 index 0000000..198fae6 --- /dev/null +++ b/src/pages/applications/api/components/api-source/ContentSourceSection.tsx @@ -0,0 +1,79 @@ + +import * as React from 'react'; +import { FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { CodeEditor } from '@/components/editor/CodeEditor'; +import { UseFormReturn } from 'react-hook-form'; +import { ApplicationAPI } from '@/types/application'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; + +interface ContentSourceSectionProps { + form: UseFormReturn & { fetchContent?: boolean }>; + codeLanguage: 'json' | 'yaml'; + setCodeLanguage: (language: 'json' | 'yaml') => void; +} + +export const ContentSourceSection: React.FC = ({ + form, + codeLanguage, + setCodeLanguage +}) => { + return ( +
+ ( + + Generated URI + + + + + Agentico URN for this API specification (auto-generated) + + + + )} + /> + +
+ Format + +
+ + ( + + Source Content + + + + + Paste or write your API specification (OpenAPI, Swagger, etc.) + + + + )} + /> +
+ ); +}; diff --git a/src/pages/applications/api/components/api-source/UriSourceSection.tsx b/src/pages/applications/api/components/api-source/UriSourceSection.tsx new file mode 100644 index 0000000..e0af362 --- /dev/null +++ b/src/pages/applications/api/components/api-source/UriSourceSection.tsx @@ -0,0 +1,129 @@ + +import * as React from 'react'; +import { FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { CodeEditor } from '@/components/editor/CodeEditor'; +import { UseFormReturn } from 'react-hook-form'; +import { ApplicationAPI } from '@/types/application'; +import { Button } from '@/components/ui/button'; +import { AlertCircle, Download } from 'lucide-react'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; + +interface UriSourceSectionProps { + form: UseFormReturn & { fetchContent?: boolean }>; + isUriValid: boolean; + onFetchContent?: () => void; + shouldFetchContent?: boolean; + setShouldFetchContent?: (value: boolean) => void; + isUriMode: boolean; + isContentMode: boolean; + codeLanguage: 'json' | 'yaml'; +} + +export const UriSourceSection: React.FC = ({ + form, + isUriValid, + onFetchContent, + shouldFetchContent, + setShouldFetchContent, + isUriMode, + isContentMode, + codeLanguage +}) => { + return ( +
+ ( + + Source URI +
+ + + + + + + + +

Please enter a valid URL (https://example.com/spec.json)

+
+
+
+ {onFetchContent && ( + + )} +
+ + Link to the API specification file (OpenAPI, Swagger, etc.) + + +
+ )} + /> + + {setShouldFetchContent && ( +
+ setShouldFetchContent(checked as boolean)} + /> + +
+ )} + + ( + + Source Content + {!field.value && isUriMode && ( + + + + The content is empty. Click the "Fetch" button above to load content from the URI, or check "Fetch content from URI when saving". + + + )} + + + + + API specification content (read-only in URI mode) + + + + )} + /> +
+ ); +}; diff --git a/src/pages/applications/api/components/api-source/index.ts b/src/pages/applications/api/components/api-source/index.ts new file mode 100644 index 0000000..59edbec --- /dev/null +++ b/src/pages/applications/api/components/api-source/index.ts @@ -0,0 +1,2 @@ + +export { ApiSourceSection } from './ApiSourceSection'; diff --git a/src/pages/applications/api/components/api-source/utils.ts b/src/pages/applications/api/components/api-source/utils.ts new file mode 100644 index 0000000..b431c9b --- /dev/null +++ b/src/pages/applications/api/components/api-source/utils.ts @@ -0,0 +1,23 @@ + +import { UseFormReturn } from 'react-hook-form'; +import { ApplicationAPI } from '@/types/application'; + +/** + * Generates a standardized URN for API content + */ +export function generateURN( + form: UseFormReturn & { fetchContent?: boolean; }>, + apiSlug: string, + organizationSlug: string, + applicationSlug: string, + apiVersion: string +) { + const name = form.getValues('name') || ''; + // Use apiSlug if provided, otherwise generate from name + const slug = apiSlug || name.toLowerCase().replace(/[^a-z0-9]/g, '-'); + // Generate the URN + const urn = `urn:agentico:apis:${organizationSlug}:${applicationSlug || 'app'}:${slug}:${apiVersion}`; + console.log('Generated URN:', urn); + // Set the URN as source_uri + form.setValue('source_uri', urn); +} From 36e5b79f1fef607c1f3a28a701adeb6cd2a4ddc1 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:21:41 +0000 Subject: [PATCH 13/50] Refactor application APIs hook Refactor `useApplicationApis` hook into smaller, more manageable files. This improves code organization and maintainability. --- src/components/applications/APIsList.tsx | 2 +- src/hooks/application-apis/index.ts | 3 + .../application-apis/useApplicationApi.tsx | 34 ++++ .../useApplicationApis.tsx | 153 +++--------------- src/hooks/application-apis/utils.ts | 84 ++++++++++ src/hooks/index.ts | 15 ++ src/pages/applications/api/ApiFormPage.tsx | 2 +- 7 files changed, 159 insertions(+), 134 deletions(-) create mode 100644 src/hooks/application-apis/index.ts create mode 100644 src/hooks/application-apis/useApplicationApi.tsx rename src/hooks/{ => application-apis}/useApplicationApis.tsx (51%) create mode 100644 src/hooks/application-apis/utils.ts create mode 100644 src/hooks/index.ts diff --git a/src/components/applications/APIsList.tsx b/src/components/applications/APIsList.tsx index 8d7947e..1aea573 100644 --- a/src/components/applications/APIsList.tsx +++ b/src/components/applications/APIsList.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router'; -import { useApplicationApis } from '@/hooks/useApplicationApis'; +import { useApplicationApis } from '@/hooks/application-apis'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; diff --git a/src/hooks/application-apis/index.ts b/src/hooks/application-apis/index.ts new file mode 100644 index 0000000..ffdddb5 --- /dev/null +++ b/src/hooks/application-apis/index.ts @@ -0,0 +1,3 @@ + +export { useApplicationApis } from './useApplicationApis'; +export { useApplicationApi } from './useApplicationApi'; diff --git a/src/hooks/application-apis/useApplicationApi.tsx b/src/hooks/application-apis/useApplicationApi.tsx new file mode 100644 index 0000000..74d3eec --- /dev/null +++ b/src/hooks/application-apis/useApplicationApi.tsx @@ -0,0 +1,34 @@ + +import { useQuery } from '@tanstack/react-query'; +import { supabase } from '@/integrations/supabase/client'; +import { useAuth } from '../useAuth'; +import type { ApplicationAPI } from '@/types/application'; +import { processApiData } from './utils'; + +/** + * Hook for fetching a single API by ID + */ +export function useApplicationApi(id?: string) { + const { session } = useAuth(); + + return useQuery({ + queryKey: ['application-api', id], + queryFn: async () => { + if (!id) return null; + + const { data, error } = await supabase + .from('application_apis') + .select('*') + .eq('id', id) + .single(); + + if (error) { + console.error('Error fetching API:', error); + throw error; + } + + return processApiData(data) as ApplicationAPI; + }, + enabled: !!id, + }); +} diff --git a/src/hooks/useApplicationApis.tsx b/src/hooks/application-apis/useApplicationApis.tsx similarity index 51% rename from src/hooks/useApplicationApis.tsx rename to src/hooks/application-apis/useApplicationApis.tsx index 8256e3c..d60f279 100644 --- a/src/hooks/useApplicationApis.tsx +++ b/src/hooks/application-apis/useApplicationApis.tsx @@ -1,10 +1,14 @@ + import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { supabase } from '@/integrations/supabase/client'; -import { useAuth } from './useAuth'; +import { useAuth } from '../useAuth'; import { toast } from 'sonner'; import type { ApplicationAPI } from '@/types/application'; -import { compressContent, decompressContent, base64ToUint8Array, uint8ArrayToBase64, fetchContentFromUri } from '@/utils/apiContentUtils'; +import { processApiData, compressApiContent, fetchAndCompressContent } from './utils'; +/** + * Hook for managing APIs for a specific application + */ export function useApplicationApis(applicationId?: string) { const { session } = useAuth(); const queryClient = useQueryClient(); @@ -24,31 +28,9 @@ export function useApplicationApis(applicationId?: string) { .order('created_at', { ascending: false }); if (error) throw error; - + // Process the APIs to handle binary data - return data.map((api: any) => { - // The source_content comes as base64 from Supabase when it's bytea - if (api.source_content) { - try { - console.log('Processing API content for:', api.name); - const compressedData = base64ToUint8Array(api.source_content); - api.source_content = decompressContent(compressedData); - console.log('Successfully decompressed content:', api.source_content.substring(0, 100) + '...'); - } catch (err) { - console.error('Error processing API content for:', api.name, err); - // Try to handle raw content if decompression fails - try { - // Attempt to just decode the base64 string directly - api.source_content = atob(api.source_content); - console.log('Fallback to direct base64 decode successful'); - } catch (decodeErr) { - console.error('Fallback decode also failed:', decodeErr); - api.source_content = ''; // Reset if all decompression fails - } - } - } - return api as ApplicationAPI; - }); + return data.map((api: any) => processApiData(api)); }, enabled: !!applicationId, }); @@ -58,34 +40,11 @@ export function useApplicationApis(applicationId?: string) { mutationFn: async (apiData: Partial & { fetchContent?: boolean }) => { if (!session?.user) throw new Error('Authentication required'); - // Extract the fetchContent flag and remove it from the data const { fetchContent, ...restData } = apiData; - let contentToSave = restData.source_content || ''; - let contentFormat = restData.content_format || 'json'; - - // If fetchContent is true and we have a source_uri, fetch the content - if (fetchContent && restData.source_uri) { - try { - const { content, format } = await fetchContentFromUri(restData.source_uri); - contentToSave = content; - contentFormat = format; - } catch (error) { - console.error('Failed to fetch content from URI:', error); - toast.error(`Failed to fetch content from URI: ${error.message}`); - } - } - - // Compress the content if it exists - let compressedContent = null; - if (contentToSave) { - try { - compressedContent = uint8ArrayToBase64(compressContent(contentToSave)); - } catch (error) { - console.error('Error compressing content:', error); - toast.error(`Error compressing content: ${error.message}`); - throw error; - } - } + + // Handle content fetching and compression + const { compressedContent, contentToSave, contentFormat } = + await fetchAndCompressContent(fetchContent, restData); const { data, error } = await supabase .from('application_apis') @@ -131,7 +90,7 @@ export function useApplicationApis(applicationId?: string) { }, }); - // Update an API - fixed to properly update in Supabase + // Update an API const updateApi = useMutation({ mutationFn: async ({ id, @@ -142,21 +101,9 @@ export function useApplicationApis(applicationId?: string) { console.log('Updating API with data:', { id, fetchContent, ...data }); - // Handle source content and fetching from URI - let contentToSave = data.source_content; - let contentFormat = data.content_format || 'json'; - - // If fetchContent is true and we have a source_uri, fetch the content - if (fetchContent && data.source_uri) { - try { - const { content, format } = await fetchContentFromUri(data.source_uri); - contentToSave = content; - contentFormat = format; - } catch (error) { - console.error('Failed to fetch content from URI:', error); - toast.error(`Failed to fetch content from URI: ${error.message}`); - } - } + // Handle content fetching and compression + const { compressedContent, contentToSave, contentFormat } = + await fetchAndCompressContent(fetchContent, data); // Create update object with only fields we want to update const updateData: Record = {}; @@ -173,20 +120,10 @@ export function useApplicationApis(applicationId?: string) { if (data.protocol !== undefined) updateData.protocol = data.protocol; if (data.is_public !== undefined) updateData.is_public = data.is_public; - // Compress the content if it exists and add to update data + // Add compressed content to update data if it exists if (contentToSave !== undefined) { - try { - if (contentToSave) { - updateData.source_content = uint8ArrayToBase64(compressContent(contentToSave)); - updateData.content_format = contentFormat; - } else { - updateData.source_content = null; - } - } catch (error) { - console.error('Error compressing content:', error); - toast.error(`Error compressing content: ${error.message}`); - throw error; - } + updateData.source_content = compressedContent; + updateData.content_format = contentFormat; } // Add updated_at field @@ -194,7 +131,7 @@ export function useApplicationApis(applicationId?: string) { console.log('Final update data to be sent to Supabase:', updateData); - // Use upsert instead of update to ensure the operation succeeds + // Update the API const { error } = await supabase .from('application_apis') .update(updateData) @@ -236,7 +173,7 @@ export function useApplicationApis(applicationId?: string) { if (error) throw error; return id; }, - onSuccess: (id) => { + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['application-apis', applicationId] }); toast.success('API deleted successfully'); }, @@ -255,51 +192,3 @@ export function useApplicationApis(applicationId?: string) { deleteApi, }; } - -// Get a single API by ID -export function useApplicationApi(id?: string) { - const { session } = useAuth(); - - return useQuery({ - queryKey: ['application-api', id], - queryFn: async () => { - if (!id) return null; - - const { data, error } = await supabase - .from('application_apis') - .select('*') - .eq('id', id) - .single(); - - if (error) { - console.error('Error fetching API:', error); - throw error; - } - - // Process the binary data - if (data.source_content) { - try { - console.log('Processing single API content for:', data.name); - const compressedData = base64ToUint8Array(data.source_content); - data.source_content = decompressContent(compressedData); - console.log('Successfully decompressed content:', data.source_content.substring(0, 100) + '...'); - } catch (err) { - console.error('Error decompressing API content:', err); - // Try to handle raw content if decompression fails - try { - // Attempt to just decode the base64 string directly - data.source_content = atob(data.source_content); - console.log('Fallback to direct base64 decode successful'); - } catch (decodeErr) { - console.error('Fallback decode also failed:', decodeErr); - data.source_content = ''; // Reset if all decompression fails - } - } - } - - console.log('Fetched API data:', data); - return data as ApplicationAPI; - }, - enabled: !!id, - }); -} diff --git a/src/hooks/application-apis/utils.ts b/src/hooks/application-apis/utils.ts new file mode 100644 index 0000000..f9fd210 --- /dev/null +++ b/src/hooks/application-apis/utils.ts @@ -0,0 +1,84 @@ + +import { ApplicationAPI } from '@/types/application'; +import { + compressContent, + decompressContent, + base64ToUint8Array, + uint8ArrayToBase64, + fetchContentFromUri +} from '@/utils/apiContentUtils'; + +/** + * Processes API data from Supabase, handling binary content decompression + */ +export function processApiData(api: any): ApplicationAPI { + if (api.source_content) { + try { + console.log('Processing API content for:', api.name); + const compressedData = base64ToUint8Array(api.source_content); + api.source_content = decompressContent(compressedData); + console.log('Successfully decompressed content:', api.source_content.substring(0, 100) + '...'); + } catch (err) { + console.error('Error processing API content for:', api.name, err); + // Try to handle raw content if decompression fails + try { + // Attempt to just decode the base64 string directly + api.source_content = atob(api.source_content); + console.log('Fallback to direct base64 decode successful'); + } catch (decodeErr) { + console.error('Fallback decode also failed:', decodeErr); + api.source_content = ''; // Reset if all decompression fails + } + } + } + return api as ApplicationAPI; +} + +/** + * Compresses API content for storage in Supabase + */ +export function compressApiContent(content: string | undefined): string | null { + if (!content) return null; + + try { + return uint8ArrayToBase64(compressContent(content)); + } catch (error) { + console.error('Error compressing content:', error); + throw error; + } +} + +/** + * Fetches and compresses content from a URI if needed + */ +export async function fetchAndCompressContent( + fetchContent: boolean = false, + data: Partial +): Promise<{ + compressedContent: string | null; + contentToSave: string; + contentFormat: 'json' | 'yaml'; +}> { + let contentToSave = data.source_content || ''; + let contentFormat = data.content_format || 'json'; + let compressedContent = null; + + // If fetchContent is true and we have a source_uri, fetch the content + if (fetchContent && data.source_uri) { + try { + const { content, format } = await fetchContentFromUri(data.source_uri); + contentToSave = content; + contentFormat = format; + } catch (error: any) { + console.error('Failed to fetch content from URI:', error); + throw new Error(`Failed to fetch content from URI: ${error.message}`); + } + } + + // Compress the content if it exists + if (contentToSave) { + compressedContent = compressApiContent(contentToSave); + } + + return { compressedContent, contentToSave, contentFormat }; +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..81d09ee --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,15 @@ + +// Export all hooks for easier importing +export * from './useAuth'; +export * from './application-apis'; +export * from './useApplications'; +export * from './useApplicationMessages'; +export * from './useApplicationServices'; +export * from './useAITools'; +export * from './usePlans'; +export * from './useProjectApplications'; +export * from './useProjectTools'; +export * from './useServers'; +export * from './useOrganizations'; +export * from './use-mobile'; +export * from './use-toast'; diff --git a/src/pages/applications/api/ApiFormPage.tsx b/src/pages/applications/api/ApiFormPage.tsx index ceb4cb7..2abde0d 100644 --- a/src/pages/applications/api/ApiFormPage.tsx +++ b/src/pages/applications/api/ApiFormPage.tsx @@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button'; import { ArrowLeft } from 'lucide-react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Form } from '@/components/ui/form'; -import { useApplicationApi, useApplicationApis } from '@/hooks/useApplicationApis'; +import { useApplicationApi, useApplicationApis } from '@/hooks/application-apis'; import { useApplication } from '@/hooks/useApplications'; import { ApplicationAPI } from '@/types/application'; import { toast } from 'sonner'; From def5239b5fa6bf88a1a2f57c3e059140f6dc9ca1 Mon Sep 17 00:00:00 2001 From: Adrian Escutia Soto Date: Tue, 18 Mar 2025 17:57:00 -0500 Subject: [PATCH 14/50] [deploy] refactor: remove pako dependency and cahnge from bytea to jsonb on APIs content --- package-lock.json | 7 - package.json | 1 - src/components/layout/BreadcrumbNav.tsx | 3 +- .../application-apis/useApplicationApi.tsx | 3 +- .../application-apis/useApplicationApis.tsx | 27 +- src/hooks/application-apis/utils.ts | 59 +--- src/hooks/useApplicationApis.tsx | 282 ++++++++++++++++++ src/utils/apiContentUtils.ts | 53 ---- vite.config.ts | 2 +- 9 files changed, 302 insertions(+), 135 deletions(-) create mode 100644 src/hooks/useApplicationApis.tsx diff --git a/package-lock.json b/package-lock.json index dd8b877..c46499f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,6 @@ "input-otp": "^1.2.4", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", - "pako": "^2.1.0", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", @@ -5238,12 +5237,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, - "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", - "license": "(MIT AND Zlib)" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/package.json b/package.json index 5c8757b..9e6093f 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "input-otp": "^1.2.4", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", - "pako": "^2.1.0", "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", diff --git a/src/components/layout/BreadcrumbNav.tsx b/src/components/layout/BreadcrumbNav.tsx index 3068768..6aa7a3c 100644 --- a/src/components/layout/BreadcrumbNav.tsx +++ b/src/components/layout/BreadcrumbNav.tsx @@ -1,4 +1,3 @@ - import React from 'react'; import { useLocation, Link } from 'react-router'; import { @@ -28,7 +27,7 @@ export function BreadcrumbNav({ items }: BreadcrumbNavProps) { - + {/* */} Home diff --git a/src/hooks/application-apis/useApplicationApi.tsx b/src/hooks/application-apis/useApplicationApi.tsx index 74d3eec..09e17cd 100644 --- a/src/hooks/application-apis/useApplicationApi.tsx +++ b/src/hooks/application-apis/useApplicationApi.tsx @@ -3,7 +3,6 @@ import { useQuery } from '@tanstack/react-query'; import { supabase } from '@/integrations/supabase/client'; import { useAuth } from '../useAuth'; import type { ApplicationAPI } from '@/types/application'; -import { processApiData } from './utils'; /** * Hook for fetching a single API by ID @@ -27,7 +26,7 @@ export function useApplicationApi(id?: string) { throw error; } - return processApiData(data) as ApplicationAPI; + return data as ApplicationAPI; }, enabled: !!id, }); diff --git a/src/hooks/application-apis/useApplicationApis.tsx b/src/hooks/application-apis/useApplicationApis.tsx index d60f279..00e9c14 100644 --- a/src/hooks/application-apis/useApplicationApis.tsx +++ b/src/hooks/application-apis/useApplicationApis.tsx @@ -4,7 +4,7 @@ import { supabase } from '@/integrations/supabase/client'; import { useAuth } from '../useAuth'; import { toast } from 'sonner'; import type { ApplicationAPI } from '@/types/application'; -import { processApiData, compressApiContent, fetchAndCompressContent } from './utils'; +import { fetchContent } from './utils'; /** * Hook for managing APIs for a specific application @@ -27,24 +27,23 @@ export function useApplicationApis(applicationId?: string) { .eq('application_id', applicationId) .order('created_at', { ascending: false }); - if (error) throw error; - + if (error) throw error; // Process the APIs to handle binary data - return data.map((api: any) => processApiData(api)); + return data; }, enabled: !!applicationId, }); // Create a new API const createApi = useMutation({ - mutationFn: async (apiData: Partial & { fetchContent?: boolean }) => { + mutationFn: async (apiData: Partial & { shouldFetchContent?: boolean }) => { if (!session?.user) throw new Error('Authentication required'); - const { fetchContent, ...restData } = apiData; + const { shouldFetchContent, ...restData } = apiData; // Handle content fetching and compression - const { compressedContent, contentToSave, contentFormat } = - await fetchAndCompressContent(fetchContent, restData); + const { contentToSave, contentFormat } = + await fetchContent(shouldFetchContent, restData); const { data, error } = await supabase .from('application_apis') @@ -55,7 +54,7 @@ export function useApplicationApis(applicationId?: string) { status: restData.status || 'active', version: restData.version, source_uri: restData.source_uri, - source_content: compressedContent, + source_content: restData.source_content || contentToSave, content_format: contentFormat, tags: restData.tags || [], }) @@ -94,16 +93,16 @@ export function useApplicationApis(applicationId?: string) { const updateApi = useMutation({ mutationFn: async ({ id, - fetchContent = false, + fetchContent: shouldFetchContent = false, ...data }: Partial & { id: string, fetchContent?: boolean }) => { if (!session?.user) throw new Error('Authentication required'); - console.log('Updating API with data:', { id, fetchContent, ...data }); + console.log('Updating API with data:', { id, fetchContent: shouldFetchContent, ...data }); // Handle content fetching and compression - const { compressedContent, contentToSave, contentFormat } = - await fetchAndCompressContent(fetchContent, data); + const { contentToSave, contentFormat } = + await fetchContent(shouldFetchContent, data); // Create update object with only fields we want to update const updateData: Record = {}; @@ -122,7 +121,7 @@ export function useApplicationApis(applicationId?: string) { // Add compressed content to update data if it exists if (contentToSave !== undefined) { - updateData.source_content = compressedContent; + updateData.source_content = contentToSave; updateData.content_format = contentFormat; } diff --git a/src/hooks/application-apis/utils.ts b/src/hooks/application-apis/utils.ts index f9fd210..81aa823 100644 --- a/src/hooks/application-apis/utils.ts +++ b/src/hooks/application-apis/utils.ts @@ -1,70 +1,24 @@ import { ApplicationAPI } from '@/types/application'; import { - compressContent, - decompressContent, - base64ToUint8Array, - uint8ArrayToBase64, fetchContentFromUri } from '@/utils/apiContentUtils'; -/** - * Processes API data from Supabase, handling binary content decompression - */ -export function processApiData(api: any): ApplicationAPI { - if (api.source_content) { - try { - console.log('Processing API content for:', api.name); - const compressedData = base64ToUint8Array(api.source_content); - api.source_content = decompressContent(compressedData); - console.log('Successfully decompressed content:', api.source_content.substring(0, 100) + '...'); - } catch (err) { - console.error('Error processing API content for:', api.name, err); - // Try to handle raw content if decompression fails - try { - // Attempt to just decode the base64 string directly - api.source_content = atob(api.source_content); - console.log('Fallback to direct base64 decode successful'); - } catch (decodeErr) { - console.error('Fallback decode also failed:', decodeErr); - api.source_content = ''; // Reset if all decompression fails - } - } - } - return api as ApplicationAPI; -} - -/** - * Compresses API content for storage in Supabase - */ -export function compressApiContent(content: string | undefined): string | null { - if (!content) return null; - - try { - return uint8ArrayToBase64(compressContent(content)); - } catch (error) { - console.error('Error compressing content:', error); - throw error; - } -} - /** * Fetches and compresses content from a URI if needed */ -export async function fetchAndCompressContent( - fetchContent: boolean = false, +export async function fetchContent( + shouldFetchContent: boolean = false, data: Partial ): Promise<{ - compressedContent: string | null; contentToSave: string; contentFormat: 'json' | 'yaml'; }> { let contentToSave = data.source_content || ''; let contentFormat = data.content_format || 'json'; - let compressedContent = null; // If fetchContent is true and we have a source_uri, fetch the content - if (fetchContent && data.source_uri) { + if (shouldFetchContent && data.source_uri) { try { const { content, format } = await fetchContentFromUri(data.source_uri); contentToSave = content; @@ -75,10 +29,5 @@ export async function fetchAndCompressContent( } } - // Compress the content if it exists - if (contentToSave) { - compressedContent = compressApiContent(contentToSave); - } - - return { compressedContent, contentToSave, contentFormat }; + return { contentToSave, contentFormat }; } diff --git a/src/hooks/useApplicationApis.tsx b/src/hooks/useApplicationApis.tsx new file mode 100644 index 0000000..6571ff4 --- /dev/null +++ b/src/hooks/useApplicationApis.tsx @@ -0,0 +1,282 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { supabase } from '@/integrations/supabase/client'; +import { useAuth } from './useAuth'; +import { toast } from 'sonner'; +import type { ApplicationAPI } from '@/types/application'; +import { compressContent, decompressContent, base64ToUint8Array, uint8ArrayToBase64, fetchContentFromUri } from '@/utils/apiContentUtils'; + +export function useApplicationApis(applicationId?: string) { + const { session } = useAuth(); + const queryClient = useQueryClient(); + + const isAuthenticated = !!session?.user; + + // Fetch all APIs for an application + const { data: apis, isLoading, error } = useQuery({ + queryKey: ['application-apis', applicationId], + queryFn: async () => { + if (!applicationId) return []; + + const { data, error } = await supabase + .from('application_apis') + .select('*') + .eq('application_id', applicationId) + .order('created_at', { ascending: false }); + + if (error) throw error; + + // Process the APIs to handle binary data + return data.map((api: any) => { + if (api.source_content) { + try { + api.source_content = decompressContent(api.source_content); + } catch (err) { + console.error('Error decompressing API content:', err); + api.source_content = ''; // Reset if decompression fails + } + } + return api as ApplicationAPI; + }); + }, + enabled: !!applicationId, + }); + + // Create a new API + const createApi = useMutation({ + mutationFn: async (apiData: Partial & { fetchContent?: boolean }) => { + if (!session?.user) throw new Error('Authentication required'); + + // Extract the fetchContent flag and remove it from the data + const { fetchContent, ...restData } = apiData; + let contentToSave = restData.source_content || ''; + let contentFormat = restData.content_format || 'json'; + + // If fetchContent is true and we have a source_uri, fetch the content + if (fetchContent && restData.source_uri) { + try { + const { content, format } = await fetchContentFromUri(restData.source_uri); + contentToSave = content; + contentFormat = format; + } catch (error) { + console.error('Failed to fetch content from URI:', error); + toast.error(`Failed to fetch content from URI: ${error.message}`); + } + } + + // Compress the content if it exists + let compressedContent = null; + if (contentToSave) { + try { + compressedContent = uint8ArrayToBase64(compressContent(contentToSave)); + } catch (error) { + console.error('Error compressing content:', error); + toast.error(`Error compressing content: ${error.message}`); + throw error; + } + } + + const { data, error } = await supabase + .from('application_apis') + .insert({ + name: restData.name, + description: restData.description, + application_id: restData.application_id, + status: restData.status || 'active', + version: restData.version, + source_uri: restData.source_uri, + source_content: compressedContent, + content_format: contentFormat, + tags: restData.tags || [], + }) + .select(); + + if (error) { + console.error('Error creating API:', error); + throw error; + } + + // Return the first item if data is an array + const createdApi = Array.isArray(data) ? data[0] : data; + + // Return the data with decompressed content for immediate use + if (createdApi.source_content) { + try { + createdApi.source_content = contentToSave; + } catch (err) { + console.error('Error with returned source_content:', err); + createdApi.source_content = ''; + } + } + + return createdApi; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['application-apis', applicationId] }); + toast.success('API created successfully'); + }, + onError: (error) => { + toast.error('Error creating API: ' + error.message); + }, + }); + + // Update an API - fixed to properly update in Supabase + const updateApi = useMutation({ + mutationFn: async ({ + id, + fetchContent = false, + ...data + }: Partial & { id: string, fetchContent?: boolean }) => { + if (!session?.user) throw new Error('Authentication required'); + + console.log('Updating API with data:', { id, fetchContent, ...data }); + + // Handle source content and fetching from URI + let contentToSave = data.source_content; + let contentFormat = data.content_format || 'json'; + + // If fetchContent is true and we have a source_uri, fetch the content + if (fetchContent && data.source_uri) { + try { + const { content, format } = await fetchContentFromUri(data.source_uri); + contentToSave = content; + contentFormat = format; + } catch (error) { + console.error('Failed to fetch content from URI:', error); + toast.error(`Failed to fetch content from URI: ${error.message}`); + } + } + + // Create update object with only fields we want to update + const updateData: Record = {}; + + // Only include fields that are defined + if (data.name !== undefined) updateData.name = data.name; + if (data.description !== undefined) updateData.description = data.description; + if (data.status !== undefined) updateData.status = data.status; + if (data.version !== undefined) updateData.version = data.version; + if (data.source_uri !== undefined) updateData.source_uri = data.source_uri; + if (data.tags !== undefined) updateData.tags = data.tags; + if (data.endpoint_url !== undefined) updateData.endpoint_url = data.endpoint_url; + if (data.documentation_url !== undefined) updateData.documentation_url = data.documentation_url; + if (data.protocol !== undefined) updateData.protocol = data.protocol; + if (data.is_public !== undefined) updateData.is_public = data.is_public; + + // Compress the content if it exists and add to update data + if (contentToSave !== undefined) { + try { + if (contentToSave) { + updateData.source_content = uint8ArrayToBase64(compressContent(contentToSave)); + updateData.content_format = contentFormat; + } else { + updateData.source_content = null; + } + } catch (error) { + console.error('Error compressing content:', error); + toast.error(`Error compressing content: ${error.message}`); + throw error; + } + } + + // Add updated_at field + updateData.updated_at = new Date().toISOString(); + + console.log('Final update data to be sent to Supabase:', updateData); + + // Use upsert instead of update to ensure the operation succeeds + const { error } = await supabase + .from('application_apis') + .update(updateData) + .eq('id', id); + + if (error) { + console.error('Error updating API:', error); + throw error; + } + + console.log('API updated successfully in Supabase'); + + // Return the updated data for optimistic updates + return { id, ...data, ...updateData }; + }, + onSuccess: (data) => { + console.log('Update successful, invalidating queries', data); + queryClient.invalidateQueries({ queryKey: ['application-apis', applicationId] }); + // Also invalidate the specific API query + queryClient.invalidateQueries({ queryKey: ['application-api', data.id] }); + toast.success('API updated successfully'); + }, + onError: (error) => { + console.error('Update API error:', error); + toast.error('Error updating API: ' + error.message); + }, + }); + + // Delete an API + const deleteApi = useMutation({ + mutationFn: async (id: string) => { + if (!session?.user) throw new Error('Authentication required'); + + const { error } = await supabase + .from('application_apis') + .delete() + .eq('id', id); + + if (error) throw error; + return id; + }, + onSuccess: (id) => { + queryClient.invalidateQueries({ queryKey: ['application-apis', applicationId] }); + toast.success('API deleted successfully'); + }, + onError: (error) => { + toast.error('Error deleting API: ' + error.message); + }, + }); + + return { + apis, + isLoading, + error, + isAuthenticated, + createApi, + updateApi, + deleteApi, + }; +} + +// Get a single API by ID +export function useApplicationApi(id?: string) { + const { session } = useAuth(); + + return useQuery({ + queryKey: ['application-api', id], + queryFn: async () => { + if (!id) return null; + + const { data, error } = await supabase + .from('application_apis') + .select('*') + .eq('id', id) + .single(); + + if (error) { + console.error('Error fetching API:', error); + throw error; + } + + // Process the binary data + if (data.source_content) { + try { + data.source_content = decompressContent(data.source_content); + } catch (err) { + console.error('Error decompressing API content:', err); + data.source_content = ''; // Reset if decompression fails + } + } + + console.log('Fetched API data:', data); + return data as ApplicationAPI; + }, + enabled: !!id, + }); +} diff --git a/src/utils/apiContentUtils.ts b/src/utils/apiContentUtils.ts index 0f1dc49..93b15bf 100644 --- a/src/utils/apiContentUtils.ts +++ b/src/utils/apiContentUtils.ts @@ -1,57 +1,4 @@ -import * as pako from 'pako'; - -/** - * Convert a string to a compressed Uint8Array - */ -export const compressContent = (content: string): Uint8Array => { - try { - const data = new TextEncoder().encode(content); - return pako.deflate(data); - } catch (error) { - console.error('Error compressing content:', error); - throw new Error('Failed to compress content'); - } -}; - -/** - * Convert a compressed Uint8Array back to a string - */ -export const decompressContent = (compressedData: Uint8Array): string => { - try { - // First try with pako.inflate (for deflate compression) - try { - const decompressed = pako.inflate(compressedData); - return new TextDecoder().decode(decompressed); - } catch (inflateError) { - console.error('Error inflating content:', inflateError); - - // Fall back to try uncompressed data - try { - return new TextDecoder().decode(compressedData); - } catch (decodeError) { - console.error('Error decoding as uncompressed:', decodeError); - - // As a last resort, try UTF-8 decoding directly - let result = ''; - for (let i = 0; i < compressedData.length; i++) { - result += String.fromCharCode(compressedData[i]); - } - - // Check if the result looks like valid text - if (/^[\x20-\x7E\s]*$/.test(result)) { - return result; - } - - throw new Error('Could not decompress content with any method'); - } - } - } catch (error) { - console.error('Error decompressing content:', error); - throw new Error('Failed to decompress content'); - } -}; - /** * Converts a base64 string to a Uint8Array * @param base64 The base64 string to convert diff --git a/vite.config.ts b/vite.config.ts index 4d6f3fe..1c793eb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,7 @@ export default defineConfig(({ mode }) => ({ server: { host: "::", port: 8080, - allowedHosts: [".agentico.dev", ".lovable.app"], + allowedHosts: [".agentico.dev", ".lovable.app", ".lovableproject.com"], }, plugins: [ react(), From 6b045b28aa83b298a9dc97c928d4e7ef191c8752 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 23:07:40 +0000 Subject: [PATCH 15/50] Refactor Application Detail Page Move Services and Messages tabs to the API level and conditionally render based on source content. --- .../application-apis/useApplicationApi.tsx | 10 + src/hooks/application-apis/utils.ts | 17 + src/hooks/useApplicationMessages.tsx | 2 + src/hooks/useApplicationServices.tsx | 2 + src/integrations/supabase/types.ts | 6 +- .../applications/ApplicationDetailPage.tsx | 44 +-- src/pages/applications/api/ApiFormPage.tsx | 27 +- .../applications/api/components/ApiForm.tsx | 306 ++++++++++-------- .../api/components/ApiMessagesList.tsx | 201 ++++++++++++ .../api/components/ApiServicesList.tsx | 196 +++++++++++ 10 files changed, 639 insertions(+), 172 deletions(-) create mode 100644 src/pages/applications/api/components/ApiMessagesList.tsx create mode 100644 src/pages/applications/api/components/ApiServicesList.tsx diff --git a/src/hooks/application-apis/useApplicationApi.tsx b/src/hooks/application-apis/useApplicationApi.tsx index 09e17cd..20d8eea 100644 --- a/src/hooks/application-apis/useApplicationApi.tsx +++ b/src/hooks/application-apis/useApplicationApi.tsx @@ -26,6 +26,16 @@ export function useApplicationApi(id?: string) { throw error; } + try { + // Check if we have source_content that needs to be processed + if (data && data.source_content) { + console.log('API has source content, length:', data.source_content.length); + } + } catch (err) { + console.error('Error processing source content:', err); + // We don't throw here to avoid breaking the entire API fetch + } + return data as ApplicationAPI; }, enabled: !!id, diff --git a/src/hooks/application-apis/utils.ts b/src/hooks/application-apis/utils.ts index 81aa823..48270b6 100644 --- a/src/hooks/application-apis/utils.ts +++ b/src/hooks/application-apis/utils.ts @@ -23,6 +23,13 @@ export async function fetchContent( const { content, format } = await fetchContentFromUri(data.source_uri); contentToSave = content; contentFormat = format; + + // Log success for debugging + console.log('Successfully fetched content from URI:', { + uri: data.source_uri, + format: format, + contentLength: content.length + }); } catch (error: any) { console.error('Failed to fetch content from URI:', error); throw new Error(`Failed to fetch content from URI: ${error.message}`); @@ -31,3 +38,13 @@ export async function fetchContent( return { contentToSave, contentFormat }; } + +/** + * Validates if the source content is present and valid + */ +export function hasValidSourceContent(api?: Partial): boolean { + if (!api) return false; + + // Check if source_content exists and is not empty + return !!(api.source_content && api.source_content.trim().length > 0); +} diff --git a/src/hooks/useApplicationMessages.tsx b/src/hooks/useApplicationMessages.tsx index 74b21ed..9fbca3c 100644 --- a/src/hooks/useApplicationMessages.tsx +++ b/src/hooks/useApplicationMessages.tsx @@ -41,6 +41,7 @@ export function useApplicationMessages(applicationId?: string) { title: messageData.title, content: messageData.content, application_id: messageData.application_id, + api_id: messageData.api_id, message_type: messageData.message_type || 'notification', status: messageData.status || 'unread', }) @@ -77,6 +78,7 @@ export function useApplicationMessages(applicationId?: string) { title: data.title, content: data.content, message_type: data.message_type, + api_id: data.api_id, status: data.status, updated_at: new Date().toISOString(), }) diff --git a/src/hooks/useApplicationServices.tsx b/src/hooks/useApplicationServices.tsx index f3fa128..171f5d3 100644 --- a/src/hooks/useApplicationServices.tsx +++ b/src/hooks/useApplicationServices.tsx @@ -41,6 +41,7 @@ export function useApplicationServices(applicationId?: string) { name: serviceData.name, description: serviceData.description, application_id: serviceData.application_id, + api_id: serviceData.api_id, status: serviceData.status || 'active', service_type: serviceData.service_type, tags: serviceData.tags || [], @@ -79,6 +80,7 @@ export function useApplicationServices(applicationId?: string) { description: data.description, status: data.status, service_type: data.service_type, + api_id: data.api_id, tags: data.tags, updated_at: new Date().toISOString(), }) diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 677c75a..08ec939 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -92,7 +92,7 @@ export type Database = { name: string protocol: string | null slug: string | null - source_content: string | null + source_content: Json | null source_uri: string | null status: string | null tags: string[] | null @@ -109,7 +109,7 @@ export type Database = { name: string protocol?: string | null slug?: string | null - source_content?: string | null + source_content?: Json | null source_uri?: string | null status?: string | null tags?: string[] | null @@ -126,7 +126,7 @@ export type Database = { name?: string protocol?: string | null slug?: string | null - source_content?: string | null + source_content?: Json | null source_uri?: string | null status?: string | null tags?: string[] | null diff --git a/src/pages/applications/ApplicationDetailPage.tsx b/src/pages/applications/ApplicationDetailPage.tsx index 865b509..5adf2d1 100644 --- a/src/pages/applications/ApplicationDetailPage.tsx +++ b/src/pages/applications/ApplicationDetailPage.tsx @@ -4,11 +4,9 @@ import { useParams, useNavigate } from 'react-router'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { ArrowLeft, PlusCircle, Settings, Code, MessageSquare } from 'lucide-react'; +import { ArrowLeft, PlusCircle, Settings, Code } from 'lucide-react'; import { useApplication } from '@/hooks/useApplications'; import APIsList from '@/components/applications/APIsList'; -import ServicesList from '@/components/applications/ServicesList'; -import MessagesList from '@/components/applications/MessagesList'; import ApplicationSettings from '@/components/applications/ApplicationSettings'; import { Skeleton } from '@/components/ui/skeleton'; import { useToast } from '@/components/ui/use-toast'; @@ -115,16 +113,10 @@ export default function ApplicationDetailPage() {
- + APIs - - Services - - - Messages - Settings @@ -144,38 +136,6 @@ export default function ApplicationDetailPage() { - -
-

Application Services

- -
- -
- - -
-

Application Messages

- -
- -
- diff --git a/src/pages/applications/api/ApiFormPage.tsx b/src/pages/applications/api/ApiFormPage.tsx index 2abde0d..781e267 100644 --- a/src/pages/applications/api/ApiFormPage.tsx +++ b/src/pages/applications/api/ApiFormPage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router'; +import { useParams, useNavigate, useLocation } from 'react-router'; import { useForm } from 'react-hook-form'; import { Button } from '@/components/ui/button'; import { ArrowLeft } from 'lucide-react'; @@ -16,6 +16,9 @@ import { BreadcrumbNav } from '@/components/layout/BreadcrumbNav'; export default function ApiFormPage() { const { applicationId, apiId } = useParams<{ applicationId: string; apiId?: string }>(); const navigate = useNavigate(); + const location = useLocation(); + const searchParams = new URLSearchParams(location.search); + const tabFromQuery = searchParams.get('tab'); const isNew = !apiId; const { data: api, isLoading: isLoadingApi } = useApplicationApi(apiId); @@ -26,6 +29,7 @@ export default function ApiFormPage() { const [sourceType, setSourceType] = useState<'uri' | 'content'>('uri'); const [codeLanguage, setCodeLanguage] = useState<'json' | 'yaml'>('json'); const [shouldFetchContent, setShouldFetchContent] = useState(false); + const [activeTab, setActiveTab] = useState(tabFromQuery || 'details'); const form = useForm & { fetchContent?: boolean }>({ defaultValues: { @@ -69,6 +73,24 @@ export default function ApiFormPage() { } }, [api, form, isNew]); + // Update URL when tab changes without full page reload + useEffect(() => { + const newSearchParams = new URLSearchParams(location.search); + if (activeTab !== 'details') { + newSearchParams.set('tab', activeTab); + } else { + newSearchParams.delete('tab'); + } + + const newSearch = newSearchParams.toString(); + const newPath = `${location.pathname}${newSearch ? `?${newSearch}` : ''}`; + + // Only update if the path would change + if (newPath !== location.pathname + location.search) { + navigate(newPath, { replace: true }); + } + }, [activeTab, location.pathname, location.search, navigate]); + const onSubmit = async (data: Partial & { fetchContent?: boolean }) => { if (!applicationId) { toast.error('Application ID is required'); @@ -195,6 +217,9 @@ export default function ApiFormPage() { organizationSlug={application?.organization_slug} apiVersion={form.watch('version') || '1.0.0'} apiSlug={apiSlug} + activeTab={activeTab} + setActiveTab={setActiveTab} + apiId={apiId} /> diff --git a/src/pages/applications/api/components/ApiForm.tsx b/src/pages/applications/api/components/ApiForm.tsx index 61e0870..635ff46 100644 --- a/src/pages/applications/api/components/ApiForm.tsx +++ b/src/pages/applications/api/components/ApiForm.tsx @@ -9,6 +9,10 @@ import { Textarea } from '@/components/ui/textarea'; import { ApplicationAPI } from '@/types/application'; import { ApiSourceSection } from './api-source'; import TagsSelector from '@/components/applications/TagSelector'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Code, Server, MessageSquare } from 'lucide-react'; +import ApiServicesList from './ApiServicesList'; +import ApiMessagesList from './ApiMessagesList'; interface ApiFormProps { form: UseFormReturn & { fetchContent?: boolean }>; @@ -27,6 +31,9 @@ interface ApiFormProps { organizationSlug?: string; apiVersion?: string; apiSlug?: string; + activeTab?: string; + setActiveTab?: (tab: string) => void; + apiId?: string; } export function ApiForm({ @@ -45,140 +52,187 @@ export function ApiForm({ applicationSlug, organizationSlug, apiVersion, - apiSlug + apiSlug, + activeTab = 'details', + setActiveTab, + apiId }: ApiFormProps) { + // Function to handle tab change + const handleTabChange = (value: string) => { + if (setActiveTab) { + setActiveTab(value); + } + }; + + // Determine if we should show the services and messages tabs + const hasSourceContent = form.watch('source_content') ? true : false; + return ( -
-
- ( - - API Name * - - - - - +
+ + + + Details + + {!isNew && hasSourceContent && ( + <> + + Services + + + Messages + + )} - /> + - ( - - API Version - - - - - - )} - /> -
- - ( - - Description - -