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 && (
+
+ {createButtonLabel}
+
+ )}
+
+ )}
+ {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.
-
-
Create Application
+ 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.
-
-
Create AI Tool
+ 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) }
-
- Format
-
+ {!readOnly && (
+
+ Format
+
+ )}
);
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 (
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 (
) : (
+
(
+
+ Generated URI
+
+
+
+
+ Automatically generated Agentico URN for this API specification
+
+
+
+ )}
+ />
+
Format
Date: Tue, 18 Mar 2025 04:45:47 -0500
Subject: [PATCH 08/50] refactor: update import statements for consistency and
clarity
---
src/components/editor/CodeEditor.tsx | 6 ++----
src/pages/applications/api/components/ApiSourceSection.tsx | 2 +-
2 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/components/editor/CodeEditor.tsx b/src/components/editor/CodeEditor.tsx
index a51ca44..e3b7c1e 100644
--- a/src/components/editor/CodeEditor.tsx
+++ b/src/components/editor/CodeEditor.tsx
@@ -1,5 +1,5 @@
-import React, { useState, useEffect } from 'react';
+import { ChangeEvent, useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Check, Copy, FileJson, FileText } from 'lucide-react';
@@ -29,9 +29,7 @@ export function CodeEditor({ value, onChange, language = 'json', className, read
setTimeout(() => setCopied(false), 2000);
};
- const handleChange = (e: React.ChangeEvent) => {
- if (readOnly) return;
-
+ const handleChange = (e: ChangeEvent) => {
const newValue = e.target.value;
setLocalValue(newValue);
onChange(newValue);
diff --git a/src/pages/applications/api/components/ApiSourceSection.tsx b/src/pages/applications/api/components/ApiSourceSection.tsx
index 3c355c2..a50300f 100644
--- a/src/pages/applications/api/components/ApiSourceSection.tsx
+++ b/src/pages/applications/api/components/ApiSourceSection.tsx
@@ -1,5 +1,5 @@
-import React from 'react';
+import * as React from 'react';
import { FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
From 063e1060699a419a3dbfefa9c330e239895c6ca7 Mon Sep 17 00:00:00 2001
From: Adrian Escutia Soto
Date: Tue, 18 Mar 2025 10:50:41 -0500
Subject: [PATCH 09/50] feat: add URI validation and tooltip feedback in
ApiSourceSection
---
.../api/components/ApiSourceSection.tsx | 109 ++++++++++++------
1 file changed, 73 insertions(+), 36 deletions(-)
diff --git a/src/pages/applications/api/components/ApiSourceSection.tsx b/src/pages/applications/api/components/ApiSourceSection.tsx
index a50300f..9ef226e 100644
--- a/src/pages/applications/api/components/ApiSourceSection.tsx
+++ b/src/pages/applications/api/components/ApiSourceSection.tsx
@@ -1,8 +1,6 @@
-
import * as React from 'react';
import { FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
-import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { CodeEditor } from '@/components/editor/CodeEditor';
import { UseFormReturn } from 'react-hook-form';
@@ -11,6 +9,7 @@ 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 }>;
@@ -41,43 +40,69 @@ export const ApiSourceSection: React.FC = ({
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]);
- 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);
+ // 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:');
}
- }, [sourceType, form, applicationSlug, organizationSlug, apiVersion, apiSlug]);
+ }
+
+ // 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 form.getValues('source_uri') && 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
-
setSourceType(value as 'uri' | 'content')}
- className="flex space-x-4"
- >
-
-
- External Source URI
-
-
-
- Inline Source Content
-
-
-
+ {/* onValueChange={(value) => setSourceType(value as 'uri' | 'content')} */}
{sourceType === 'uri' ? (
= ({
Source URI
-
-
-
+
+
+
+
+
+
+
+
+ Please enter a valid URL (https://example.com/spec.json)
+
+
+
{onFetchContent && (
Fetch
@@ -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 && (
-
- Fetch
-
- )}
-
-
- Link to the API specification file (OpenAPI, Swagger, etc.)
-
-
-
- )}
- />
-
- {setShouldFetchContent && (
-
- setShouldFetchContent(checked as boolean)}
- />
-
- Fetch content from URI when saving
-
-
- )}
-
- (
-
- 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/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 (
+
+ );
+};
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 && (
+
+ Fetch
+
+ )}
+
+
+ Link to the API specification file (OpenAPI, Swagger, etc.)
+
+
+
+ )}
+ />
+
+ {setShouldFetchContent && (
+
+ setShouldFetchContent(checked as boolean)}
+ />
+
+ Fetch content from URI when saving
+
+
+ )}
+
+ (
+
+ 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
-
{
- if (orgSlug && appSlug) {
- navigate(`/apps/${orgSlug}@${appSlug}/services/new`);
- } else if (id) {
- navigate(`/applications/${id}/services/new`);
- }
- }}>
- New Service
-
-
-
-
-
-
-
-
Application Messages
-
{
- if (orgSlug && appSlug) {
- navigate(`/apps/${orgSlug}@${appSlug}/messages/new`);
- } else if (id) {
- navigate(`/applications/${id}/messages/new`);
- }
- }}>
- New Message
-
-
-
-
-
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 (
-