updated task
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+4529
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "vite-react-typescript-starter",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"reactflow": "^11.10.4",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.11",
|
||||
"globals": "^15.9.0",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.3.0",
|
||||
"vite": "^5.4.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
import { Sidebar } from './components/Sidebar';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="w-full h-screen">
|
||||
<ReactFlowProvider>
|
||||
<div className="flex h-screen">
|
||||
<Sidebar />
|
||||
<div className="flex-1 bg-white" />
|
||||
</div>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { Lock, Globe, ArrowUpDown, Code, Database, Variable } from 'lucide-react';
|
||||
|
||||
const components = [
|
||||
{ id: 'auth', name: 'Authentication', icon: Lock },
|
||||
{ id: 'url', name: 'URL Route', icon: Globe },
|
||||
{ id: 'output', name: 'Output Data', icon: ArrowUpDown },
|
||||
{ id: 'logic', name: 'Logic', icon: Code },
|
||||
{ id: 'variable', name: 'Variable', icon: Variable },
|
||||
{ id: 'db-find', name: 'DB Find', icon: Database },
|
||||
{ id: 'db-insert', name: 'DB Insert', icon: Database },
|
||||
{ id: 'db-update', name: 'DB Update', icon: Database },
|
||||
{ id: 'db-delete', name: 'DB Delete', icon: Database },
|
||||
{ id: 'db-query', name: 'DB Query', icon: Database },
|
||||
];
|
||||
|
||||
export function ComponentsPanel() {
|
||||
const onDragStart = (event: React.DragEvent, nodeType: string) => {
|
||||
event.dataTransfer.setData('application/reactflow', nodeType);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 border-r border-gray-200 p-4 bg-white">
|
||||
<h3 className="text-sm font-medium text-gray-900 mb-3">Components</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{components.map((component) => {
|
||||
const Icon = component.icon;
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
className="flex flex-col items-center justify-center p-3 bg-gray-50 rounded-lg cursor-move hover:bg-gray-100 transition-colors"
|
||||
onDragStart={(event) => onDragStart(event, component.id)}
|
||||
draggable
|
||||
>
|
||||
<Icon className="w-6 h-6 mb-2" />
|
||||
<span className="text-sm text-center">{component.name}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,786 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { X, Plus, Trash } from "lucide-react";
|
||||
import { Node } from "reactflow";
|
||||
import { useFlowStore } from "../store/flowStore";
|
||||
|
||||
interface ConfigPanelProps {
|
||||
node: Node | null;
|
||||
onClose: () => void;
|
||||
onUpdateNode: (id: string, data: any) => void;
|
||||
}
|
||||
|
||||
interface Field {
|
||||
name: string;
|
||||
type: string;
|
||||
validation?: string;
|
||||
}
|
||||
|
||||
const getDefaultDataForType = (type: string) => {
|
||||
const baseData = {
|
||||
label: type.charAt(0).toUpperCase() + type.slice(1).replace("-", " ")
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case "variable":
|
||||
return {
|
||||
...baseData,
|
||||
name: "",
|
||||
type: "string",
|
||||
defaultValue: ""
|
||||
};
|
||||
|
||||
case "url":
|
||||
return {
|
||||
...baseData,
|
||||
method: "GET",
|
||||
path: "",
|
||||
fields: [],
|
||||
queryFields: []
|
||||
};
|
||||
|
||||
case "auth":
|
||||
return {
|
||||
...baseData,
|
||||
authType: "bearer",
|
||||
tokenVar: ""
|
||||
};
|
||||
|
||||
case "output":
|
||||
return {
|
||||
...baseData,
|
||||
outputType: "definition",
|
||||
statusCode: 200,
|
||||
fields: [],
|
||||
responseRaw: ""
|
||||
};
|
||||
|
||||
case "logic":
|
||||
return {
|
||||
...baseData,
|
||||
code: ""
|
||||
};
|
||||
|
||||
case "db-find":
|
||||
case "db-query":
|
||||
return {
|
||||
...baseData,
|
||||
model: "",
|
||||
operation: "findMany",
|
||||
query: "",
|
||||
resultVar: "result"
|
||||
};
|
||||
|
||||
case "db-insert":
|
||||
return {
|
||||
...baseData,
|
||||
model: "",
|
||||
variables: "",
|
||||
resultVar: "result"
|
||||
};
|
||||
|
||||
case "db-update":
|
||||
case "db-delete":
|
||||
return {
|
||||
...baseData,
|
||||
model: "",
|
||||
idField: "id",
|
||||
variables: "",
|
||||
resultVar: "result"
|
||||
};
|
||||
|
||||
default:
|
||||
return baseData;
|
||||
}
|
||||
};
|
||||
|
||||
export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
|
||||
const { models, updateNode } = useFlowStore();
|
||||
const [newField, setNewField] = useState<Field>({ name: "", type: "string" });
|
||||
const [newQueryField, setNewQueryField] = useState<Field>({
|
||||
name: "",
|
||||
type: "string",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (node) {
|
||||
// Initialize node data with defaults if not already set
|
||||
const defaultData = getDefaultDataForType(node.type);
|
||||
const newData = {
|
||||
...defaultData,
|
||||
...node.data // This will override defaults with any existing data
|
||||
};
|
||||
|
||||
// Only update if the data is different
|
||||
if (JSON.stringify(newData) !== JSON.stringify(node.data)) {
|
||||
onUpdateNode(node.id, newData);
|
||||
updateNode(node.id, newData);
|
||||
}
|
||||
}
|
||||
}, [node?.id, node?.type]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("ConfigPanel re-rendered with node:", node);
|
||||
}, [node]);
|
||||
|
||||
if (!node) return null;
|
||||
console.log("what up");
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||
) => {
|
||||
if (!node) return;
|
||||
|
||||
console.log("Handling change for:", e.target.name, "with value:", e.target.value);
|
||||
|
||||
const newData = {
|
||||
...node.data,
|
||||
[e.target.name]: e.target.value,
|
||||
};
|
||||
|
||||
console.log("New data to update:", newData);
|
||||
|
||||
onUpdateNode(node.id, newData);
|
||||
updateNode(node.id, newData);
|
||||
};
|
||||
|
||||
const handleArrayChange = (
|
||||
index: number,
|
||||
field: string,
|
||||
value: string,
|
||||
arrayName: string
|
||||
) => {
|
||||
const array = [...node.data[arrayName]];
|
||||
array[index] = { ...array[index], [field]: value };
|
||||
const newData = {
|
||||
...node.data,
|
||||
[arrayName]: array,
|
||||
};
|
||||
|
||||
onUpdateNode(node.id, newData);
|
||||
updateNode(node.id, newData);
|
||||
};
|
||||
|
||||
const addField = (arrayName: string) => {
|
||||
const fieldToAdd = arrayName === "queryFields" ? newQueryField : newField;
|
||||
if (!fieldToAdd.name.trim()) return;
|
||||
|
||||
const array = [...node.data[arrayName], { ...fieldToAdd }];
|
||||
const newData = {
|
||||
...node.data,
|
||||
[arrayName]: array,
|
||||
};
|
||||
|
||||
onUpdateNode(node.id, newData);
|
||||
updateNode(node.id, newData);
|
||||
|
||||
// Reset the appropriate state
|
||||
if (arrayName === "queryFields") {
|
||||
setNewQueryField({ name: "", type: "string" });
|
||||
} else {
|
||||
setNewField({ name: "", type: "string" });
|
||||
}
|
||||
};
|
||||
|
||||
const removeField = (index: number, arrayName: string) => {
|
||||
const array = [...node.data[arrayName]];
|
||||
array.splice(index, 1);
|
||||
const newData = {
|
||||
...node.data,
|
||||
[arrayName]: array,
|
||||
};
|
||||
|
||||
onUpdateNode(node.id, newData);
|
||||
updateNode(node.id, newData);
|
||||
};
|
||||
|
||||
const copyQueryFields = () => {
|
||||
const currentFields = node.data.fields || [];
|
||||
navigator.clipboard.writeText(JSON.stringify(currentFields, null, 2));
|
||||
};
|
||||
|
||||
const extractQueryParams = (path: string) => {
|
||||
const params = path.match(/:[a-zA-Z]+/g) || [];
|
||||
return params.map((param) => ({
|
||||
name: param.substring(1),
|
||||
type: "string",
|
||||
validation: "",
|
||||
}));
|
||||
};
|
||||
|
||||
const renderDatabaseFields = () => (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Model</label>
|
||||
<select
|
||||
name="model"
|
||||
value={node.data.model}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
<option value="">Select Model</option>
|
||||
{models.map((model) => (
|
||||
<option key={model.id} value={model.name}>
|
||||
{model.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Operation</label>
|
||||
<select
|
||||
name="operation"
|
||||
value={node.data.operation}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
<option value="findMany">Find Many</option>
|
||||
<option value="findOne">Find One</option>
|
||||
<option value="findFirst">Find First</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">SQL Query</label>
|
||||
<textarea
|
||||
name="query"
|
||||
value={node.data.query}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded h-32 font-mono text-sm"
|
||||
placeholder="SELECT * FROM table WHERE id = :id"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Save Result In</label>
|
||||
<input
|
||||
type="text"
|
||||
name="resultVar"
|
||||
value={node.data.resultVar}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="result"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderFields = () => {
|
||||
switch (node.type) {
|
||||
case "auth":
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Auth Type
|
||||
</label>
|
||||
<select
|
||||
name="authType"
|
||||
value={node.data.authType}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
<option value="bearer">Bearer Token</option>
|
||||
<option value="basic">Basic Auth</option>
|
||||
<option value="jwt">JWT</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Token Variable
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="tokenVar"
|
||||
value={node.data.tokenVar}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="token"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case "url":
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Method</label>
|
||||
<select
|
||||
name="method"
|
||||
value={node.data.method}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
{["GET", "POST", "PUT", "DELETE", "PATCH"].map((method) => (
|
||||
<option key={method} value={method}>
|
||||
{method}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Route Path
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="path"
|
||||
value={node.data.path}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="/api/users/:id"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Body Fields
|
||||
</label>
|
||||
{node.data.fields?.length > 0 && (
|
||||
<div className="mb-2 p-2 bg-gray-50 rounded text-sm">
|
||||
{node.data.fields.map((field: Field) => (
|
||||
<div key={field.name} className="text-gray-600">
|
||||
{field.name}: {field.type}
|
||||
{field.validation ? ` (${field.validation})` : ""}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(node.data.fields || []).map((field: Field, index: number) => (
|
||||
<div key={index} className="flex gap-1 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={field.name}
|
||||
onBlur={(e) =>
|
||||
handleArrayChange(index, "name", e.target.value, "fields")
|
||||
}
|
||||
className="flex-1 p-2 border rounded text-sm"
|
||||
placeholder="Field name"
|
||||
/>
|
||||
<select
|
||||
value={field.type}
|
||||
onBlur={(e) =>
|
||||
handleArrayChange(index, "type", e.target.value, "fields")
|
||||
}
|
||||
className="w-20 p-2 border rounded text-sm"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Bool</option>
|
||||
<option value="date">Date</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => removeField(index, "fields")}
|
||||
className="p-2 text-red-500 hover:bg-red-50 rounded"
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-1 mt-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newField.name}
|
||||
onBlur={(e) =>
|
||||
setNewField({ ...newField, name: e.target.value })
|
||||
}
|
||||
className="flex-1 p-2 border rounded text-sm"
|
||||
placeholder="New field name"
|
||||
/>
|
||||
<select
|
||||
value={newField.type}
|
||||
onBlur={(e) =>
|
||||
setNewField({ ...newField, type: e.target.value })
|
||||
}
|
||||
className="w-20 p-2 border rounded text-sm"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Bool</option>
|
||||
<option value="date">Date</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (newField.name.trim()) {
|
||||
addField("fields");
|
||||
}
|
||||
}}
|
||||
className="p-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Query Parameters
|
||||
</label>
|
||||
{node.data.queryFields?.length > 0 && (
|
||||
<div className="mb-2 p-2 bg-gray-50 rounded text-sm">
|
||||
{node.data.queryFields.map((field: Field) => (
|
||||
<div key={field.name} className="text-gray-600">
|
||||
{field.name}: {field.type}
|
||||
{field.validation ? ` (${field.validation})` : ""}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(node.data.queryFields || []).map(
|
||||
(field: Field, index: number) => (
|
||||
<div key={index} className="flex gap-1 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={field.name}
|
||||
onBlur={(e) =>
|
||||
handleArrayChange(
|
||||
index,
|
||||
"name",
|
||||
e.target.value,
|
||||
"queryFields"
|
||||
)
|
||||
}
|
||||
className="flex-1 p-2 border rounded text-sm"
|
||||
placeholder="Field name"
|
||||
/>
|
||||
<select
|
||||
value={field.type}
|
||||
onBlur={(e) =>
|
||||
handleArrayChange(
|
||||
index,
|
||||
"type",
|
||||
e.target.value,
|
||||
"queryFields"
|
||||
)
|
||||
}
|
||||
className="w-20 p-2 border rounded text-sm"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Bool</option>
|
||||
<option value="date">Date</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => removeField(index, "queryFields")}
|
||||
className="p-2 text-red-500 hover:bg-red-50 rounded"
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<div className="flex gap-1 mt-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newQueryField.name}
|
||||
onBlur={(e) =>
|
||||
setNewQueryField({ ...newQueryField, name: e.target.value })
|
||||
}
|
||||
className="flex-1 p-2 border rounded text-sm"
|
||||
placeholder="New query param"
|
||||
/>
|
||||
<select
|
||||
value={newQueryField.type}
|
||||
onBlur={(e) =>
|
||||
setNewQueryField({ ...newQueryField, type: e.target.value })
|
||||
}
|
||||
className="w-20 p-2 border rounded text-sm"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Bool</option>
|
||||
<option value="date">Date</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (newQueryField.name.trim()) {
|
||||
const updatedQueryFields = [
|
||||
...(node.data.queryFields || []),
|
||||
{ ...newQueryField },
|
||||
];
|
||||
onUpdateNode(node.id, {
|
||||
...node.data,
|
||||
queryFields: updatedQueryFields,
|
||||
});
|
||||
setNewQueryField({ name: "", type: "string" });
|
||||
}
|
||||
}}
|
||||
className="p-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case "output":
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Output Type</label>
|
||||
<select
|
||||
name="outputType"
|
||||
value={node.data.outputType}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
<option value="definition">Definition</option>
|
||||
<option value="mockup">Mockup</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{node.data.outputType === "mockup" ? (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Response Raw</label>
|
||||
<textarea
|
||||
name="responseRaw"
|
||||
value={node.data.responseRaw}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded h-32"
|
||||
placeholder="Enter raw response here..."
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Fields</label>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{(node.data.fields || []).map((field: Field, index: number) => (
|
||||
<div key={index} className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={field.name}
|
||||
onBlur={(e) =>
|
||||
handleArrayChange(index, "name", e.target.value, "fields")
|
||||
}
|
||||
className="flex-1 p-2 border rounded text-sm"
|
||||
placeholder="Field name"
|
||||
/>
|
||||
<select
|
||||
value={field.type}
|
||||
onBlur={(e) =>
|
||||
handleArrayChange(index, "type", e.target.value, "fields")
|
||||
}
|
||||
className="w-24 p-2 border rounded text-sm"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="object">Object</option>
|
||||
<option value="array">Array</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => removeField(index, "fields")}
|
||||
className="p-2 text-red-500 hover:bg-red-50 rounded"
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newField.name}
|
||||
onBlur={(e) =>
|
||||
setNewField({ ...newField, name: e.target.value })
|
||||
}
|
||||
className="flex-1 p-2 border rounded text-sm"
|
||||
placeholder="New field name"
|
||||
/>
|
||||
<select
|
||||
value={newField.type}
|
||||
onBlur={(e) =>
|
||||
setNewField({ ...newField, type: e.target.value })
|
||||
}
|
||||
className="w-24 p-2 border rounded text-sm"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="object">Object</option>
|
||||
<option value="array">Array</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (newField.name.trim()) {
|
||||
const updatedFields = [
|
||||
...(node.data.fields || []),
|
||||
{ ...newField },
|
||||
];
|
||||
onUpdateNode(node.id, {
|
||||
...node.data,
|
||||
fields: updatedFields,
|
||||
});
|
||||
setNewField({ name: "", type: newField.type }); // Use the selected type here
|
||||
}
|
||||
}}
|
||||
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Status Code</label>
|
||||
<input
|
||||
type="number"
|
||||
name="statusCode"
|
||||
value={node.data.statusCode}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="200"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case "variable":
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Variable Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={node.data.name}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="myVariable"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Type</label>
|
||||
<select
|
||||
name="type"
|
||||
value={node.data.type}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
<option value="string">String</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="object">Object</option>
|
||||
<option value="array">Array</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Default Value
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="defaultValue"
|
||||
value={node.data.defaultValue}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Default value"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case "logic":
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
JavaScript Code
|
||||
</label>
|
||||
<textarea
|
||||
name="code"
|
||||
value={node.data.code}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded h-40 font-mono text-sm"
|
||||
placeholder="// Write your JavaScript code here"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "db-find":
|
||||
return renderDatabaseFields();
|
||||
|
||||
case "db-query":
|
||||
return (
|
||||
<>
|
||||
{renderDatabaseFields()}
|
||||
<div className="mb-4">
|
||||
<button
|
||||
onClick={copyQueryFields}
|
||||
className="px-3 py-1 text-sm bg-gray-100 hover:bg-gray-200 rounded"
|
||||
>
|
||||
Copy Fields
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case "db-insert":
|
||||
return (
|
||||
<>
|
||||
{renderDatabaseFields()}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Variables
|
||||
</label>
|
||||
<textarea
|
||||
name="variables"
|
||||
value={node.data.variables}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded h-20 font-mono text-sm"
|
||||
placeholder="name: string age: number"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case "db-update":
|
||||
case "db-delete":
|
||||
return (
|
||||
<>
|
||||
{renderDatabaseFields()}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">ID Field</label>
|
||||
<input
|
||||
type="text"
|
||||
name="idField"
|
||||
value={node.data.idField}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="id"
|
||||
/>
|
||||
</div>
|
||||
{node.type === "db-update" && (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Variables
|
||||
</label>
|
||||
<textarea
|
||||
name="variables"
|
||||
value={node.data.variables}
|
||||
onBlur={handleChange}
|
||||
className="w-full p-2 border rounded h-20 font-mono text-sm"
|
||||
placeholder="name: string age: number"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed right-0 top-0 h-full w-80 bg-white border-l border-gray-200 p-4 shadow-lg transform transition-transform overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">Configure Node</h3>
|
||||
<button onClick={onClose} className="p-1 hover:bg-gray-100 rounded">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
{renderFields()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import { Lock, Globe, ArrowUpDown, Code, Database, Variable } from 'lucide-react';
|
||||
|
||||
const CustomNode = ({ data, type }: NodeProps) => {
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case 'auth':
|
||||
return <Lock className="w-5 h-5" />;
|
||||
case 'url':
|
||||
return <Globe className="w-5 h-5" />;
|
||||
case 'output':
|
||||
return <ArrowUpDown className="w-5 h-5" />;
|
||||
case 'logic':
|
||||
return <Code className="w-5 h-5" />;
|
||||
case 'variable':
|
||||
return <Variable className="w-5 h-5" />;
|
||||
case 'db-find':
|
||||
case 'db-insert':
|
||||
case 'db-update':
|
||||
case 'db-delete':
|
||||
case 'db-query':
|
||||
return <Database className="w-5 h-5" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative px-4 py-2 shadow-md rounded-md bg-white border-2 border-gray-200 min-w-[180px] cursor-grab active:cursor-grabbing">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="!w-3 !h-3 !bg-blue-500 !border-2 !border-white"
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<div className="rounded-full w-8 h-8 flex items-center justify-center bg-gray-50 mr-2">
|
||||
{getIcon()}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-bold">{data.label}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{type.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!w-3 !h-3 !bg-blue-500 !border-2 !border-white"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CustomNode);
|
||||
@@ -0,0 +1,227 @@
|
||||
import React, { useState } from "react";
|
||||
import { X, Plus } from "lucide-react";
|
||||
import { useFlowStore } from "../store/flowStore";
|
||||
|
||||
interface DefaultTable {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
fields: {
|
||||
name: string;
|
||||
type: string;
|
||||
defaultValue?: string;
|
||||
mapping?: string;
|
||||
validation?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const defaultTables: DefaultTable[] = [
|
||||
{
|
||||
id: "cms",
|
||||
name: "CMS",
|
||||
description: "Content Management System table for storing dynamic content",
|
||||
fields: [
|
||||
{ name: "id", type: "primary key" },
|
||||
{ name: "key", type: "string", validation: "required" },
|
||||
{
|
||||
name: "type",
|
||||
type: "mapping",
|
||||
mapping: "0:Text,1:Number,2:Image,3:Raw",
|
||||
validation: "0",
|
||||
},
|
||||
{ name: "value", type: "long text" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "user",
|
||||
name: "User",
|
||||
description: "User management table with authentication details",
|
||||
fields: [
|
||||
{ name: "id", type: "primary key" },
|
||||
{ name: "email", type: "string", validation: "required,email" },
|
||||
{
|
||||
name: "login_type",
|
||||
type: "mapping",
|
||||
mapping: "0:Regular,1:Google,2:Microsoft,3:Apple",
|
||||
defaultValue: "0",
|
||||
},
|
||||
{ name: "role_id", type: "string" },
|
||||
{ name: "data", type: "json" },
|
||||
{
|
||||
name: "status",
|
||||
type: "mapping",
|
||||
mapping: "0:Active,1:Inactive,2:Suspend",
|
||||
defaultValue: "0",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface DefaultTablesModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function DefaultTablesModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
}: DefaultTablesModalProps) {
|
||||
const [selectedTables, setSelectedTables] = useState<string[]>([]);
|
||||
const [includeAdminRole, setIncludeAdminRole] = useState(true);
|
||||
const [includeMemberRole, setIncludeMemberRole] = useState(true);
|
||||
const { addModel, addRole } = useFlowStore();
|
||||
|
||||
const handleToggleTable = (tableId: string) => {
|
||||
setSelectedTables((prev) =>
|
||||
prev.includes(tableId)
|
||||
? prev.filter((id) => id !== tableId)
|
||||
: [...prev, tableId]
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddTables = () => {
|
||||
selectedTables.forEach((tableId) => {
|
||||
const table = defaultTables.find((t) => t.id === tableId);
|
||||
if (table) {
|
||||
addModel({
|
||||
id: `model_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: table.name,
|
||||
fields: table.fields,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add roles based on checkbox selection
|
||||
if (selectedTables.includes("user")) {
|
||||
if (includeAdminRole) {
|
||||
addRole({
|
||||
id: `role_admin_${Date.now()}`,
|
||||
name: "Admin",
|
||||
slug: "admin",
|
||||
permissions: {
|
||||
authRequired: true,
|
||||
routes: [],
|
||||
canCreateUsers: true,
|
||||
canEditUsers: true,
|
||||
canDeleteUsers: true,
|
||||
canManageRoles: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMemberRole) {
|
||||
addRole({
|
||||
id: `role_member_${Date.now()}`,
|
||||
name: "Member",
|
||||
slug: "member",
|
||||
permissions: {
|
||||
authRequired: true,
|
||||
routes: [],
|
||||
canCreateUsers: false,
|
||||
canEditUsers: false,
|
||||
canDeleteUsers: false,
|
||||
canManageRoles: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg w-full max-w-2xl max-h-[80vh] overflow-hidden">
|
||||
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h2 className="text-lg font-semibold">Add Default Tables</h2>
|
||||
<button onClick={onClose} className="p-1 hover:bg-gray-100 rounded">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 overflow-y-auto">
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Select the default tables you would like to add to your project:
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{defaultTables.map((table) => (
|
||||
<div
|
||||
key={table.id}
|
||||
className="border rounded-lg p-4 hover:border-blue-500 transition-colors"
|
||||
>
|
||||
<label className="flex items-start space-x-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTables.includes(table.id)}
|
||||
onChange={() => handleToggleTable(table.id)}
|
||||
className="mt-1 rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<div>
|
||||
<h3 className="font-medium">{table.name}</h3>
|
||||
<p className="text-sm text-gray-600">{table.description}</p>
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
Fields: {table.fields.map((f) => f.name).join(", ")}
|
||||
</div>
|
||||
|
||||
{/* Add role options when User table is selected */}
|
||||
{table.id === "user" && selectedTables.includes("user") && (
|
||||
<div className="mt-3 pl-2 border-l-2 border-gray-200">
|
||||
<p className="text-sm font-medium text-gray-700 mb-2">
|
||||
Include roles:
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center space-x-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeAdminRole}
|
||||
onChange={(e) =>
|
||||
setIncludeAdminRole(e.target.checked)
|
||||
}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span>Admin Role</span>
|
||||
</label>
|
||||
<label className="flex items-center space-x-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeMemberRole}
|
||||
onChange={(e) =>
|
||||
setIncludeMemberRole(e.target.checked)
|
||||
}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span>Member Role</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-200 flex justify-end gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddTables}
|
||||
disabled={selectedTables.length === 0}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-blue-300 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Selected Tables
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
ReactFlowProvider,
|
||||
addEdge,
|
||||
Connection,
|
||||
useReactFlow,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
} from 'reactflow';
|
||||
import { useFlowStore } from '../store/flowStore';
|
||||
import { ConfigPanel } from './ConfigPanel';
|
||||
import CustomNode from './CustomNode';
|
||||
|
||||
const nodeTypes = {
|
||||
auth: CustomNode,
|
||||
url: CustomNode,
|
||||
output: CustomNode,
|
||||
logic: CustomNode,
|
||||
variable: CustomNode,
|
||||
'db-find': CustomNode,
|
||||
'db-insert': CustomNode,
|
||||
'db-update': CustomNode,
|
||||
'db-delete': CustomNode,
|
||||
'db-query': CustomNode,
|
||||
};
|
||||
|
||||
export function FlowEditor() {
|
||||
const { project } = useReactFlow();
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
setNodes,
|
||||
setEdges,
|
||||
selectedNode,
|
||||
setSelectedNode,
|
||||
updateNodeData,
|
||||
} = useFlowStore();
|
||||
|
||||
const onNodesChange = React.useCallback(
|
||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
[setNodes]
|
||||
);
|
||||
|
||||
const onEdgesChange = React.useCallback(
|
||||
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onConnect = React.useCallback(
|
||||
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onNodeClick = React.useCallback(
|
||||
(_: React.MouseEvent, node: any) => {
|
||||
setSelectedNode(node);
|
||||
},
|
||||
[setSelectedNode]
|
||||
);
|
||||
|
||||
const onPaneClick = React.useCallback(() => {
|
||||
setSelectedNode(null);
|
||||
}, [setSelectedNode]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<div className="flex-1">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
<ConfigPanel
|
||||
node={selectedNode}
|
||||
onClose={() => setSelectedNode(null)}
|
||||
onUpdateNode={updateNodeData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { X, Plus, Trash } from "lucide-react";
|
||||
import { useFlowStore } from "../store/flowStore";
|
||||
|
||||
interface Field {
|
||||
name: string;
|
||||
type: string;
|
||||
defaultValue: string;
|
||||
validation: string;
|
||||
validationOptions?: {
|
||||
pattern?: string;
|
||||
enum?: string[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
};
|
||||
mapping?: string;
|
||||
}
|
||||
|
||||
interface ModelModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
model?: {
|
||||
id: string;
|
||||
name: string;
|
||||
fields: Field[];
|
||||
};
|
||||
}
|
||||
|
||||
const fieldTypes = [
|
||||
"primary key",
|
||||
"string",
|
||||
"long text",
|
||||
"integer",
|
||||
"double",
|
||||
"big number",
|
||||
"boolean",
|
||||
"date",
|
||||
"datetime",
|
||||
"uuid",
|
||||
"json",
|
||||
"mapping",
|
||||
];
|
||||
|
||||
const validationRules = [
|
||||
"required",
|
||||
"email",
|
||||
"url",
|
||||
"min",
|
||||
"max",
|
||||
"pattern",
|
||||
"enum",
|
||||
"length",
|
||||
"minLength",
|
||||
"maxLength",
|
||||
"positive",
|
||||
"negative",
|
||||
"integer",
|
||||
"decimal",
|
||||
"alphanumeric",
|
||||
"uuid",
|
||||
"json",
|
||||
"date",
|
||||
"phone",
|
||||
];
|
||||
|
||||
const initialNewField: Field = {
|
||||
name: "",
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
validation: "",
|
||||
validationOptions: {},
|
||||
};
|
||||
|
||||
const createInitialModelData = () => ({
|
||||
id: `model_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: "",
|
||||
fields: [],
|
||||
});
|
||||
|
||||
export function ModelModal({ isOpen, onClose, model }: ModelModalProps) {
|
||||
const [modelData, setModelData] = useState<{
|
||||
id: string;
|
||||
name: string;
|
||||
fields: Field[];
|
||||
}>(createInitialModelData());
|
||||
const [newField, setNewField] = useState<Field>(initialNewField);
|
||||
const [createCrudApis, setCreateCrudApis] = useState(false); // New state for CRUD API checkbox
|
||||
|
||||
const { addModel, updateModel, addRoute } = useFlowStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (model) {
|
||||
setModelData({
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
fields: [...model.fields],
|
||||
});
|
||||
} else {
|
||||
setModelData(createInitialModelData());
|
||||
}
|
||||
}, [model, isOpen]);
|
||||
|
||||
const handleAddField = () => {
|
||||
if (newField.name) {
|
||||
setModelData({
|
||||
...modelData,
|
||||
fields: [...modelData.fields, { ...newField }],
|
||||
});
|
||||
setNewField(initialNewField);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveField = (index: number) => {
|
||||
const updatedFields = [...modelData.fields];
|
||||
updatedFields.splice(index, 1);
|
||||
setModelData({
|
||||
...modelData,
|
||||
fields: updatedFields,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (modelData.name) {
|
||||
if (model) {
|
||||
updateModel(modelData);
|
||||
} else {
|
||||
addModel(modelData);
|
||||
if (createCrudApis) {
|
||||
// Create GET route for fetching all records
|
||||
const uniqueId = Date.now(); // Generate a unique identifier based on the current timestamp
|
||||
const getRoute = {
|
||||
id: `route_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: `Get All ${modelData.name}`,
|
||||
method: 'GET',
|
||||
url: `/api/${modelData.name.toLowerCase()}`,
|
||||
flowData: {
|
||||
nodes: [
|
||||
{
|
||||
id: `url_node_${uniqueId}`,
|
||||
type: 'url',
|
||||
position: { x: 100, y: 100 },
|
||||
data: {
|
||||
label: 'URL',
|
||||
path: `/api/${modelData.name.toLowerCase()}`,
|
||||
method: 'GET'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: `db_find_node_${uniqueId}`,
|
||||
type: 'db-find',
|
||||
position: { x: 100, y: 200 },
|
||||
data: {
|
||||
label: 'Database Find',
|
||||
model: modelData.name,
|
||||
operation: 'findMany',
|
||||
query: `SELECT * FROM ${modelData.name}`,
|
||||
resultVar: `${modelData.name}Result`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: `output_node_${uniqueId}`,
|
||||
type: 'output',
|
||||
position: { x: 100, y: 300 },
|
||||
data: {
|
||||
label: 'Output',
|
||||
outputType: 'definition',
|
||||
fields: modelData.fields.map(field => ({
|
||||
name: field.name,
|
||||
type: field.type === 'primary key' ? 'number' :
|
||||
field.type === 'long text' ? 'string' :
|
||||
field.type === 'big number' ? 'number' :
|
||||
field.type
|
||||
})),
|
||||
statusCode: 200
|
||||
}
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: `url-to-db_${uniqueId}`,
|
||||
source: `url_node_${uniqueId}`,
|
||||
target: `db_find_node_${uniqueId}`
|
||||
},
|
||||
{
|
||||
id: `db-to-output_${uniqueId}`,
|
||||
source: `db_find_node_${uniqueId}`,
|
||||
target: `output_node_${uniqueId}`
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const getOneRoute = {
|
||||
id: `route_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: `Get One ${modelData.name}`,
|
||||
method: 'GET',
|
||||
url: `/api/${modelData.name.toLowerCase()}/:id`,
|
||||
flowData: {
|
||||
nodes: [
|
||||
{
|
||||
id: `url_node_${uniqueId}_1`,
|
||||
type: 'url',
|
||||
position: { x: 100, y: 100 },
|
||||
data: {
|
||||
label: 'URL',
|
||||
path: `/api/${modelData.name.toLowerCase()}/:id`,
|
||||
method: 'GET'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: `db_find_node_${uniqueId}_1`,
|
||||
type: 'db-find',
|
||||
position: { x: 100, y: 200 },
|
||||
data: {
|
||||
label: 'Database Find',
|
||||
model: modelData.name,
|
||||
operation: 'findOne',
|
||||
query: `SELECT * FROM ${modelData.name} WHERE id=id`,
|
||||
resultVar: `${modelData.name}OneResult`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: `output_node_${uniqueId}_1`,
|
||||
type: 'output',
|
||||
position: { x: 100, y: 300 },
|
||||
data: {
|
||||
label: 'Output',
|
||||
outputType: 'definition',
|
||||
fields: modelData.fields.map(field => ({
|
||||
name: field.name,
|
||||
type: field.type === 'primary key' ? 'number' :
|
||||
field.type === 'long text' ? 'string' :
|
||||
field.type === 'big number' ? 'number' :
|
||||
field.type
|
||||
})),
|
||||
statusCode: 200
|
||||
}
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: `url-to-db_${uniqueId}_1`,
|
||||
source: `url_node_${uniqueId}_1`,
|
||||
target: `db_find_node_${uniqueId}_1`
|
||||
},
|
||||
{
|
||||
id: `db-to-output_${uniqueId}_1`,
|
||||
source: `db_find_node_${uniqueId}_1`,
|
||||
target: `output_node_${uniqueId}_1`
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const deleteRoute = {
|
||||
id: `route_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: `Delete One ${modelData.name}`,
|
||||
method: 'DELETE',
|
||||
url: `/api/${modelData.name.toLowerCase()}/:id`,
|
||||
flowData: {
|
||||
nodes: [
|
||||
{
|
||||
id: `url_node_${uniqueId}_1`,
|
||||
type: 'url',
|
||||
position: { x: 100, y: 100 },
|
||||
data: {
|
||||
label: 'URL',
|
||||
path: `/api/${modelData.name.toLowerCase()}/:id`,
|
||||
method: 'DELETE'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: `db_find_node_${uniqueId}_2`,
|
||||
type: 'db-delete',
|
||||
position: { x: 100, y: 200 },
|
||||
data: {
|
||||
label: 'Database Delete',
|
||||
model: modelData.name,
|
||||
operation: 'findOne',
|
||||
query: `DELETE FROM ${modelData.name} WHERE id=id`,
|
||||
resultVar: `${modelData.name}DeleteResult`
|
||||
}
|
||||
},
|
||||
{
|
||||
id: `output_node_${uniqueId}_2`,
|
||||
type: 'output',
|
||||
position: { x: 100, y: 300 },
|
||||
data: {
|
||||
label: 'Output',
|
||||
outputType: 'definition',
|
||||
fields: [
|
||||
{name: "error", type: "boolean"},
|
||||
{name: "id", type: "integer"}
|
||||
],
|
||||
statusCode: 200
|
||||
}
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: `url-to-db_${uniqueId}_1`,
|
||||
source: `url_node_${uniqueId}_1`,
|
||||
target: `db_find_node_${uniqueId}_2`
|
||||
},
|
||||
{
|
||||
id: `db-to-output_${uniqueId}_2`,
|
||||
source: `db_find_node_${uniqueId}_2`,
|
||||
target: `output_node_${uniqueId}_2 `
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
addRoute(deleteRoute as any);
|
||||
addRoute(getRoute as any);
|
||||
addRoute(getOneRoute as any);
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg w-full max-w-2xl max-h-[80vh] overflow-hidden">
|
||||
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{model ? "Edit Model" : "Add New Model"}
|
||||
</h2>
|
||||
<button onClick={onClose} className="p-1 hover:bg-gray-100 rounded">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 overflow-y-auto">
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-1">Model Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={modelData.name}
|
||||
onChange={(e) =>
|
||||
setModelData({ ...modelData, name: e.target.value })
|
||||
}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Enter model name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-2">Fields</label>
|
||||
<div className="space-y-2">
|
||||
{modelData.fields.map((field, index) => (
|
||||
<div key={index} className="flex gap-2 items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={field.name}
|
||||
readOnly
|
||||
className="flex-1 p-2 border rounded bg-gray-50"
|
||||
/>
|
||||
<span className="px-2 py-1 bg-gray-100 rounded text-sm">
|
||||
{field.type}
|
||||
</span>
|
||||
{field.validation && (
|
||||
<span className="px-2 py-1 bg-blue-100 rounded text-sm">
|
||||
{field.validation}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleRemoveField(index)}
|
||||
className="p-1 text-red-500 hover:bg-red-50 rounded"
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newField.name}
|
||||
onChange={(e) =>
|
||||
setNewField({ ...newField, name: e.target.value })
|
||||
}
|
||||
className="flex-1 p-2 border rounded"
|
||||
placeholder="Field name"
|
||||
/>
|
||||
<select
|
||||
value={newField.type}
|
||||
onChange={(e) =>
|
||||
setNewField({ ...newField, type: e.target.value })
|
||||
}
|
||||
className="w-32 p-2 border rounded"
|
||||
>
|
||||
{fieldTypes.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newField.defaultValue}
|
||||
onChange={(e) =>
|
||||
setNewField({ ...newField, defaultValue: e.target.value })
|
||||
}
|
||||
className="flex-1 p-2 border rounded"
|
||||
placeholder="Default value"
|
||||
/>
|
||||
<select
|
||||
value={newField.validation}
|
||||
onChange={(e) =>
|
||||
setNewField({ ...newField, validation: e.target.value })
|
||||
}
|
||||
className="w-32 p-2 border rounded"
|
||||
>
|
||||
<option value="">Validation</option>
|
||||
{validationRules.map((rule) => (
|
||||
<option key={rule} value={rule}>
|
||||
{rule}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{newField.type === "mapping" && (
|
||||
<input
|
||||
type="text"
|
||||
value={newField.mapping || ""}
|
||||
onChange={(e) =>
|
||||
setNewField({ ...newField, mapping: e.target.value })
|
||||
}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="key:value,key2:value2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{newField.validation &&
|
||||
[
|
||||
"pattern",
|
||||
"enum",
|
||||
"min",
|
||||
"max",
|
||||
"minLength",
|
||||
"maxLength",
|
||||
].includes(newField.validation) && (
|
||||
<div className="flex gap-2">
|
||||
{newField.validation === "pattern" && (
|
||||
<input
|
||||
type="text"
|
||||
value={newField.validationOptions?.pattern || ""}
|
||||
onChange={(e) =>
|
||||
setNewField({
|
||||
...newField,
|
||||
validationOptions: {
|
||||
...newField.validationOptions,
|
||||
pattern: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="flex-1 p-2 border rounded"
|
||||
placeholder="Regular expression pattern"
|
||||
/>
|
||||
)}
|
||||
{newField.validation === "enum" && (
|
||||
<input
|
||||
type="text"
|
||||
value={
|
||||
newField.validationOptions?.enum?.join(",") || ""
|
||||
}
|
||||
onChange={(e) =>
|
||||
setNewField({
|
||||
...newField,
|
||||
validationOptions: {
|
||||
...newField.validationOptions,
|
||||
enum: e.target.value.split(","),
|
||||
},
|
||||
})
|
||||
}
|
||||
className="flex-1 p-2 border rounded"
|
||||
placeholder="Comma-separated values"
|
||||
/>
|
||||
)}
|
||||
{["min", "max", "minLength", "maxLength"].includes(
|
||||
newField.validation
|
||||
) && (
|
||||
<input
|
||||
type="number"
|
||||
value={
|
||||
newField.validationOptions?.[
|
||||
newField.validation as keyof typeof newField.validationOptions
|
||||
] || ""
|
||||
}
|
||||
onChange={(e) =>
|
||||
setNewField({
|
||||
...newField,
|
||||
validationOptions: {
|
||||
...newField.validationOptions,
|
||||
[newField.validation]: parseFloat(e.target.value),
|
||||
},
|
||||
})
|
||||
}
|
||||
className="flex-1 p-2 border rounded"
|
||||
placeholder={`Enter ${newField.validation} value`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleAddField}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Field
|
||||
</button>
|
||||
|
||||
|
||||
<div className="flex items-center mt-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={createCrudApis}
|
||||
onChange={(e) => setCreateCrudApis(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<label className="text-sm">Create CRUD APIs</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-200 flex justify-end gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
{model ? "Update" : "Save"} Model
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Plus, Edit2 } from "lucide-react";
|
||||
import { useFlowStore } from "../store/flowStore";
|
||||
import { ModelModal } from "./ModelModal";
|
||||
import { DefaultTablesModal } from "./DefaultTablesModal";
|
||||
|
||||
export function ModelPanel() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isDefaultTablesModalOpen, setIsDefaultTablesModalOpen] =
|
||||
useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState<any>(null);
|
||||
const { models, defaultTablesShown, setDefaultTablesShown } = useFlowStore();
|
||||
|
||||
useEffect(() => {
|
||||
// Show default tables modal only if there are no models and it hasn't been shown before
|
||||
if (models.length === 0 && !defaultTablesShown) {
|
||||
setIsDefaultTablesModalOpen(true);
|
||||
setDefaultTablesShown(true); // Mark as shown
|
||||
}
|
||||
}, []); // Empty dependency array means this runs once on mount
|
||||
|
||||
const handleEditModel = (model: any) => {
|
||||
setSelectedModel(model);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedModel(null);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Model
|
||||
</button>
|
||||
|
||||
<div className="space-y-3">
|
||||
{models.map((model) => (
|
||||
<div
|
||||
key={model.id}
|
||||
className="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium">{model.name}</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{model.fields.length} field
|
||||
{model.fields.length !== 1 ? "s" : ""}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="p-1 hover:bg-gray-100 rounded"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditModel(model);
|
||||
}}
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ModelModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedModel(null);
|
||||
}}
|
||||
model={selectedModel}
|
||||
/>
|
||||
|
||||
<DefaultTablesModal
|
||||
isOpen={isDefaultTablesModalOpen}
|
||||
onClose={() => setIsDefaultTablesModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { X, Trash2 } from "lucide-react";
|
||||
import { useFlowStore } from "../store/flowStore";
|
||||
|
||||
interface Permissions {
|
||||
authRequired: boolean;
|
||||
routes: string[];
|
||||
canCreateUsers: boolean;
|
||||
canEditUsers: boolean;
|
||||
canDeleteUsers: boolean;
|
||||
canManageRoles: boolean;
|
||||
canLogin: boolean;
|
||||
canRegister: boolean;
|
||||
canGoogleLogin: boolean;
|
||||
canAppleLogin: boolean;
|
||||
canMicrosoftLogin: boolean;
|
||||
canMagicLinkLogin: boolean;
|
||||
needs2FA: boolean;
|
||||
canSetPermissions: boolean;
|
||||
}
|
||||
|
||||
interface RoleModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
role?: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
permissions: Permissions;
|
||||
};
|
||||
}
|
||||
|
||||
const createInitialFormData = () => ({
|
||||
id: `role_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: "",
|
||||
slug: "",
|
||||
permissions: {
|
||||
authRequired: false,
|
||||
routes: [],
|
||||
canCreateUsers: false,
|
||||
canEditUsers: false,
|
||||
canDeleteUsers: false,
|
||||
canManageRoles: false,
|
||||
canLogin: false,
|
||||
canRegister: false,
|
||||
canGoogleLogin: false,
|
||||
canAppleLogin: false,
|
||||
canMicrosoftLogin: false,
|
||||
canMagicLinkLogin: false,
|
||||
needs2FA: false,
|
||||
canSetPermissions: false,
|
||||
},
|
||||
});
|
||||
|
||||
export function RoleModal({ isOpen, onClose, role }: RoleModalProps) {
|
||||
const { routes, addRole, updateRole, deleteRole } = useFlowStore();
|
||||
const [formData, setFormData] = useState(createInitialFormData());
|
||||
|
||||
useEffect(() => {
|
||||
if (role) {
|
||||
setFormData(role);
|
||||
} else {
|
||||
setFormData(createInitialFormData());
|
||||
}
|
||||
}, [role, isOpen]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
if (type === "checkbox") {
|
||||
if (name.startsWith("permissions.")) {
|
||||
const permissionKey = name.split(".")[1];
|
||||
setFormData({
|
||||
...formData,
|
||||
permissions: {
|
||||
...formData.permissions,
|
||||
[permissionKey]: checked,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value,
|
||||
...(name === "name" && !role
|
||||
? {
|
||||
slug: value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, ""),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRouteChange = (routeId: string, checked: boolean) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
permissions: {
|
||||
...formData.permissions,
|
||||
routes: checked
|
||||
? [...formData.permissions.routes, routeId]
|
||||
: formData.permissions.routes.filter((id) => id !== routeId),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAllRoutes = (checked: boolean) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
permissions: {
|
||||
...formData.permissions,
|
||||
routes: checked ? routes.map((route) => route.id) : [],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (role) {
|
||||
updateRole(formData);
|
||||
} else {
|
||||
addRole(formData);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (role) {
|
||||
deleteRole(role.id);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg w-full max-w-2xl max-h-[80vh] overflow-hidden">
|
||||
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{role ? "Edit Role" : "Add New Role"}
|
||||
</h2>
|
||||
<button onClick={onClose} className="p-1 hover:bg-gray-100 rounded">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 overflow-y-auto">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Role Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Enter role name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Role Slug
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="slug"
|
||||
value={formData.slug}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="role-slug"
|
||||
readOnly={!!role}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-medium">Permissions</h4>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
formData.permissions.routes.length > 0 &&
|
||||
formData.permissions.routes.length === routes.length
|
||||
}
|
||||
onChange={(e) =>
|
||||
handleSelectAllRoutes(e.target.checked)
|
||||
}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span className="ml-2 text-sm">Select All</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="max-h-40 overflow-y-auto border rounded p-2 space-y-2">
|
||||
{routes.map((route) => (
|
||||
<label key={route.id} className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.permissions.routes.includes(
|
||||
route.id
|
||||
)}
|
||||
onChange={(e) =>
|
||||
handleRouteChange(route.id, e.target.checked)
|
||||
}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span className="ml-2 text-sm">
|
||||
{route.name} ({route.method} {route.url})
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">User Management</h4>
|
||||
<div className="space-y-2 ml-4">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions.canCreateUsers"
|
||||
checked={formData.permissions.canCreateUsers}
|
||||
onChange={handleChange}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span className="ml-2 text-sm">Can Create Users</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions.canEditUsers"
|
||||
checked={formData.permissions.canEditUsers}
|
||||
onChange={handleChange}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span className="ml-2 text-sm">Can Edit Users</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions.canDeleteUsers"
|
||||
checked={formData.permissions.canDeleteUsers}
|
||||
onChange={handleChange}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span className="ml-2 text-sm">Can Delete Users</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">
|
||||
Authentication Management
|
||||
</h4>
|
||||
<div className="space-y-2 ml-4">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions.authRequired"
|
||||
checked={formData.permissions.authRequired}
|
||||
onChange={handleChange}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span className="ml-2 text-sm">
|
||||
Authentication Required
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions.canLogin"
|
||||
checked={formData.permissions.canLogin}
|
||||
onChange={handleChange}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span className="ml-2 text-sm">Can Login</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions.canRegister"
|
||||
checked={formData.permissions.canRegister}
|
||||
onChange={handleChange}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span className="ml-2 text-sm">Can Register</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions.canGoogleLogin"
|
||||
checked={formData.permissions.canGoogleLogin}
|
||||
onChange={handleChange}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span className="ml-2 text-sm">Can Use Google Login</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions.needs2FA"
|
||||
checked={formData.permissions.needs2FA}
|
||||
onChange={handleChange}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span className="ml-2 text-sm">Requires 2FA</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions.canSetPermissions"
|
||||
checked={formData.permissions.canSetPermissions}
|
||||
onChange={handleChange}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span className="ml-2 text-sm">Can Set Permissions</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="permissions.canManageRoles"
|
||||
checked={formData.permissions.canManageRoles}
|
||||
onChange={handleChange}
|
||||
className="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<span className="ml-2 text-sm">Can Manage Roles</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-200 flex justify-between">
|
||||
{role && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="px-4 py-2 text-red-600 hover:bg-red-50 rounded flex items-center gap-2"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Delete Role
|
||||
</button>
|
||||
)}
|
||||
<div className="flex gap-2 ml-auto">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
{role ? "Update" : "Save"} Role
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Plus, Edit2 } from 'lucide-react';
|
||||
import { useFlowStore } from '../store/flowStore';
|
||||
import { RoleModal } from './RoleModal';
|
||||
|
||||
export function RolesPanel() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedRole, setSelectedRole] = useState<any>(null);
|
||||
const { roles } = useFlowStore();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedRole(null);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Role
|
||||
</button>
|
||||
|
||||
<div className="space-y-3">
|
||||
{roles.map((role) => (
|
||||
<div
|
||||
key={role.id}
|
||||
className="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium">{role.name}</h3>
|
||||
<p className="text-sm text-gray-500">{role.slug}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedRole(role);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
className="p-1 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<RoleModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedRole(null);
|
||||
}}
|
||||
role={selectedRole}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import React, { useCallback, useRef, useEffect } from "react";
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
ReactFlowProvider,
|
||||
addEdge,
|
||||
Connection,
|
||||
useReactFlow,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
Panel,
|
||||
} from "reactflow";
|
||||
import { ArrowLeft, Save } from "lucide-react";
|
||||
import { useFlowStore } from "../store/flowStore";
|
||||
import { ConfigPanel } from "./ConfigPanel";
|
||||
import { ComponentsPanel } from "./ComponentsPanel";
|
||||
import CustomNode from "./CustomNode";
|
||||
import "reactflow/dist/style.css";
|
||||
|
||||
const nodeTypes = {
|
||||
auth: CustomNode,
|
||||
url: CustomNode,
|
||||
output: CustomNode,
|
||||
logic: CustomNode,
|
||||
variable: CustomNode,
|
||||
"db-find": CustomNode,
|
||||
"db-insert": CustomNode,
|
||||
"db-update": CustomNode,
|
||||
"db-delete": CustomNode,
|
||||
"db-query": CustomNode,
|
||||
};
|
||||
|
||||
interface FlowEditorContentProps {
|
||||
route: any;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function FlowEditorContent({ route, onClose }: FlowEditorContentProps) {
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const { project } = useReactFlow();
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
setNodes,
|
||||
setEdges,
|
||||
selectedNode,
|
||||
setSelectedNode,
|
||||
updateNodeData,
|
||||
updateRoute,
|
||||
} = useFlowStore();
|
||||
|
||||
useEffect(() => {
|
||||
// Load saved flow data if it exists
|
||||
if (route.flowData) {
|
||||
setNodes(route.flowData.nodes);
|
||||
setEdges(route.flowData.edges);
|
||||
} else {
|
||||
// Create default URL node for new routes
|
||||
const defaultNode = {
|
||||
id: `node_${Date.now()}`,
|
||||
type: "url",
|
||||
position: { x: 100, y: 100 },
|
||||
data: {
|
||||
label: "URL",
|
||||
path: route.url,
|
||||
method: route.method,
|
||||
},
|
||||
};
|
||||
setNodes([defaultNode]);
|
||||
setEdges([]);
|
||||
}
|
||||
}, [route, setNodes, setEdges]);
|
||||
|
||||
const onNodesChange = useCallback(
|
||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
[setNodes]
|
||||
);
|
||||
|
||||
const onEdgesChange = useCallback(
|
||||
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const type = event.dataTransfer.getData("application/reactflow");
|
||||
if (!type || !reactFlowWrapper.current) return;
|
||||
|
||||
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||
const position = project({
|
||||
x: event.clientX - reactFlowBounds.left - 75,
|
||||
y: event.clientY - reactFlowBounds.top - 25,
|
||||
});
|
||||
|
||||
const newNode = {
|
||||
id: `node_${Date.now()}`,
|
||||
type,
|
||||
position,
|
||||
data: {
|
||||
label: type.charAt(0).toUpperCase() + type.slice(1).replace("-", " "),
|
||||
},
|
||||
};
|
||||
|
||||
setNodes((nds) => [...nds, newNode]);
|
||||
},
|
||||
[project, setNodes]
|
||||
);
|
||||
|
||||
const onNodeClick = useCallback(
|
||||
(_: React.MouseEvent, node: any) => {
|
||||
setSelectedNode(node);
|
||||
},
|
||||
[setSelectedNode]
|
||||
);
|
||||
|
||||
const onPaneClick = useCallback(() => {
|
||||
setSelectedNode(null);
|
||||
}, [setSelectedNode]);
|
||||
|
||||
const handleSave = () => {
|
||||
updateRoute({
|
||||
...route,
|
||||
flowData: {
|
||||
nodes,
|
||||
edges,
|
||||
},
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="border-b border-gray-200 p-4 flex items-center justify-between bg-white">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
Back to Routes
|
||||
</button>
|
||||
<h2 className="ml-4 text-lg font-semibold">
|
||||
{route.name} ({route.method} {route.url})
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<ComponentsPanel />
|
||||
<div className="flex-1 h-full" ref={reactFlowWrapper}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
nodeTypes={nodeTypes}
|
||||
deleteKeyCode="Delete"
|
||||
multiSelectionKeyCode="Control"
|
||||
selectionKeyCode="Shift"
|
||||
snapToGrid={true}
|
||||
snapGrid={[15, 15]}
|
||||
defaultViewport={{ x: 0, y: 0, zoom: 1 }}
|
||||
fitView
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
{selectedNode && (
|
||||
<ConfigPanel
|
||||
node={selectedNode}
|
||||
onClose={() => setSelectedNode(null)}
|
||||
onUpdateNode={updateNodeData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RouteFlowEditor({ route, onClose }: FlowEditorContentProps) {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<FlowEditorContent route={route} onClose={onClose} />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { X, Trash2 } from "lucide-react";
|
||||
import { useFlowStore } from "../store/flowStore";
|
||||
|
||||
interface RouteModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
route?: {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
method: string;
|
||||
};
|
||||
}
|
||||
|
||||
const createInitialFormData = () => ({
|
||||
id: `route_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: "",
|
||||
url: "",
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
export function RouteModal({ isOpen, onClose, route }: RouteModalProps) {
|
||||
const { addRoute, updateRoute, deleteRoute } = useFlowStore();
|
||||
const [formData, setFormData] = useState(createInitialFormData());
|
||||
|
||||
useEffect(() => {
|
||||
if (route) {
|
||||
setFormData(route);
|
||||
} else {
|
||||
setFormData(createInitialFormData());
|
||||
}
|
||||
}, [route, isOpen]);
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (route) {
|
||||
updateRoute(formData);
|
||||
} else {
|
||||
addRoute(formData);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (route) {
|
||||
deleteRoute(route.id);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg w-full max-w-md">
|
||||
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{route ? "Edit Route" : "Add New Route"}
|
||||
</h2>
|
||||
<button onClick={onClose} className="p-1 hover:bg-gray-100 rounded">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Route Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Enter route name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Method</label>
|
||||
<select
|
||||
name="method"
|
||||
value={formData.method}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="DELETE">DELETE</option>
|
||||
<option value="PATCH">PATCH</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">URL</label>
|
||||
<input
|
||||
type="text"
|
||||
name="url"
|
||||
value={formData.url}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="/api/resource/:id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-200 flex justify-between">
|
||||
{route && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="px-4 py-2 text-red-600 hover:bg-red-50 rounded flex items-center gap-2"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Delete Route
|
||||
</button>
|
||||
)}
|
||||
<div className="flex gap-2 ml-auto">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||
>
|
||||
{route ? "Update" : "Save"} Route
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useState } from "react";
|
||||
import { Plus, Edit2, Code } from "lucide-react";
|
||||
import { useFlowStore } from "../store/flowStore";
|
||||
import { RouteModal } from "./RouteModal";
|
||||
import { RouteFlowEditor } from "./RouteFlowEditor";
|
||||
|
||||
export function RoutesPanel() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [selectedRoute, setSelectedRoute] = useState<any>(null);
|
||||
const [editingRoute, setEditingRoute] = useState<any>(null);
|
||||
const { routes } = useFlowStore();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{!editingRoute ? (
|
||||
<>
|
||||
<div className="p-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedRoute(null);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
className="flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 w-full"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Route
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4">
|
||||
<div className="space-y-3">
|
||||
{routes.map((route) => (
|
||||
<div
|
||||
key={route.id}
|
||||
className="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-medium">{route.name}</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{route.method} {route.url}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedRoute(route);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
className="p-1 hover:bg-gray-100 rounded"
|
||||
title="Edit Route"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingRoute(route)}
|
||||
className="p-1 hover:bg-gray-100 rounded"
|
||||
title="Edit Components"
|
||||
>
|
||||
<Code className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RouteModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedRoute(null);
|
||||
}}
|
||||
route={selectedRoute}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="fixed inset-0 bg-white">
|
||||
<RouteFlowEditor
|
||||
route={editingRoute}
|
||||
onClose={() => setEditingRoute(null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import React from "react";
|
||||
import { Save } from "lucide-react";
|
||||
import { useFlowStore } from "../store/flowStore";
|
||||
import { TranslationService } from "../services/TranslationService";
|
||||
|
||||
export function SettingsForm() {
|
||||
const { settings, updateSettings, routes } = useFlowStore();
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||
) => {
|
||||
updateSettings({
|
||||
...settings,
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
updateSettings(settings);
|
||||
};
|
||||
|
||||
const handleExportConfiguration = () => {
|
||||
const models = useFlowStore.getState().models;
|
||||
const roles = useFlowStore.getState().roles;
|
||||
const routes = useFlowStore.getState().routes;
|
||||
|
||||
const configuration = {
|
||||
models: models.map((model) => TranslationService.translateModel(model)),
|
||||
roles: roles.map((role) => ({
|
||||
name: role.name,
|
||||
slug: role.slug,
|
||||
permissions: {
|
||||
...role.permissions,
|
||||
routes: role.permissions.routes
|
||||
.map((routeId) => {
|
||||
const route = routes.find((r) => r.id === routeId);
|
||||
return route
|
||||
? {
|
||||
method: route.method,
|
||||
url: route.url,
|
||||
}
|
||||
: null;
|
||||
})
|
||||
.filter(Boolean),
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(configuration, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "configuration.json";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Global Key</label>
|
||||
<input
|
||||
type="text"
|
||||
name="globalKey"
|
||||
value={settings?.globalKey || ""}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Enter global key"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Database Type</label>
|
||||
<select
|
||||
name="databaseType"
|
||||
value={settings?.databaseType || ""}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
<option value="">Select Database Type</option>
|
||||
<option value="mysql">MySQL</option>
|
||||
<option value="postgresql">PostgreSQL</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Authentication Type
|
||||
</label>
|
||||
<select
|
||||
name="authType"
|
||||
value={settings?.authType || ""}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
<option value="session">Session</option>
|
||||
<option value="jwt">JWT</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Timezone</label>
|
||||
<select
|
||||
name="timezone"
|
||||
value={settings?.timezone || ""}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
>
|
||||
<option value="UTC">UTC</option>
|
||||
<option value="America/New_York">America/New_York</option>
|
||||
<option value="Europe/London">Europe/London</option>
|
||||
<option value="Asia/Tokyo">Asia/Tokyo</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium">
|
||||
Database Credentials
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="dbHost"
|
||||
value={settings?.dbHost || ""}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded mb-2"
|
||||
placeholder="Host"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="dbPort"
|
||||
value={settings?.dbPort || ""}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded mb-2"
|
||||
placeholder="Port"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="dbUser"
|
||||
value={settings?.dbUser || ""}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded mb-2"
|
||||
placeholder="Username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="dbPassword"
|
||||
value={settings?.dbPassword || ""}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded mb-2"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="dbName"
|
||||
value={settings?.dbName || ""}
|
||||
onChange={handleChange}
|
||||
className="w-full p-2 border rounded"
|
||||
placeholder="Database Name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
Save Settings
|
||||
</button>
|
||||
|
||||
<div className="mt-6">
|
||||
<button
|
||||
onClick={handleExportConfiguration}
|
||||
className="w-full px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
|
||||
>
|
||||
Export Configuration
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Lock, Globe, ArrowUpDown, Code, Database, Variable, Plus, X } from 'lucide-react';
|
||||
import { ModelPanel } from './ModelPanel';
|
||||
import { SettingsForm } from './SettingsForm';
|
||||
import { RolesPanel } from './RolesPanel';
|
||||
import { RoutesPanel } from './RoutesPanel';
|
||||
|
||||
type Tab = 'models' | 'roles' | 'routes' | 'settings';
|
||||
|
||||
export function Sidebar() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('models');
|
||||
|
||||
return (
|
||||
<div className="w-64 bg-white border-r border-gray-200 flex flex-col h-full">
|
||||
{/* Tab Buttons */}
|
||||
<div className="flex flex-wrap border-b border-gray-200">
|
||||
<button
|
||||
className={`flex-1 py-3 text-sm font-medium ${
|
||||
activeTab === 'models'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
onClick={() => setActiveTab('models')}
|
||||
>
|
||||
Models
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 py-3 text-sm font-medium ${
|
||||
activeTab === 'roles'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
onClick={() => setActiveTab('roles')}
|
||||
>
|
||||
Roles
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 py-3 text-sm font-medium ${
|
||||
activeTab === 'routes'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
onClick={() => setActiveTab('routes')}
|
||||
>
|
||||
Routes
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 py-3 text-sm font-medium ${
|
||||
activeTab === 'settings'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
onClick={() => setActiveTab('settings')}
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{activeTab === 'models' && <ModelPanel />}
|
||||
{activeTab === 'roles' && <RolesPanel />}
|
||||
{activeTab === 'routes' && <RoutesPanel />}
|
||||
{activeTab === 'settings' && <SettingsForm />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useFlowContext } from '../store/FlowContext';
|
||||
|
||||
const SomeComponent = () => {
|
||||
const { nodes, setNodes } = useFlowContext();
|
||||
|
||||
// Example usage
|
||||
const addNode = () => {
|
||||
setNodes([...nodes, { id: 'new-node', data: {}, position: { x: 0, y: 0 } }]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={addNode}>Add Node</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SomeComponent;
|
||||
@@ -0,0 +1,55 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.react-flow__node {
|
||||
@apply select-none;
|
||||
}
|
||||
|
||||
.react-flow__handle {
|
||||
width: 8px !important;
|
||||
height: 8px !important;
|
||||
background: #3b82f6 !important;
|
||||
border: 2px solid white !important;
|
||||
}
|
||||
|
||||
.react-flow__handle-left {
|
||||
left: -4px !important;
|
||||
}
|
||||
|
||||
.react-flow__handle-right {
|
||||
right: -4px !important;
|
||||
}
|
||||
|
||||
.react-flow__node-default {
|
||||
width: auto !important;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.react-flow__edge-path {
|
||||
stroke: #64748b;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.react-flow__edge.selected .react-flow__edge-path {
|
||||
stroke: #3b82f6;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.react-flow__node.selected {
|
||||
box-shadow: 0 0 0 2px #3b82f6;
|
||||
}
|
||||
|
||||
.react-flow__controls {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.react-flow__panel {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.react-flow {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import { FlowProvider } from './store/FlowContext';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<FlowProvider>
|
||||
<App />
|
||||
</FlowProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,85 @@
|
||||
interface ModelData {
|
||||
id: string;
|
||||
name: string;
|
||||
fields: Field[];
|
||||
}
|
||||
|
||||
interface Field {
|
||||
name: string;
|
||||
type: string;
|
||||
defaultValue: string;
|
||||
validation: string;
|
||||
validationOptions?: {
|
||||
pattern?: string;
|
||||
enum?: string[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
};
|
||||
mapping?: string;
|
||||
}
|
||||
|
||||
export class TranslationService {
|
||||
static translateModel(model: ModelData): any {
|
||||
return {
|
||||
name: model.name,
|
||||
fields: model.fields.map((field) => ({
|
||||
name: field.name,
|
||||
type: this.translateFieldType(field.type),
|
||||
isPrimaryKey: field.type === "primary key",
|
||||
validation: this.translateValidation(field),
|
||||
defaultValue: field.defaultValue || null,
|
||||
...(field.mapping ? { mapping: this.parseMapping(field.mapping) } : {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
private static translateFieldType(type: string): string {
|
||||
if (type === "primary key") {
|
||||
return "INTEGER";
|
||||
}
|
||||
|
||||
const typeMap: { [key: string]: string } = {
|
||||
string: "STRING",
|
||||
"long text": "TEXT",
|
||||
integer: "INTEGER",
|
||||
double: "DOUBLE",
|
||||
"big number": "BIGINT",
|
||||
boolean: "BOOLEAN",
|
||||
date: "DATE",
|
||||
datetime: "DATETIME",
|
||||
uuid: "UUID",
|
||||
json: "JSON",
|
||||
mapping: "MAPPING",
|
||||
};
|
||||
return typeMap[type] || type.toUpperCase();
|
||||
}
|
||||
|
||||
private static translateValidation(field: Field): any {
|
||||
if (!field.validation) return null;
|
||||
|
||||
const validation: any = {
|
||||
type: field.validation,
|
||||
};
|
||||
|
||||
if (field.validationOptions) {
|
||||
Object.assign(validation, field.validationOptions);
|
||||
}
|
||||
|
||||
return validation;
|
||||
}
|
||||
|
||||
private static parseMapping(mapping: string): Record<string, string> {
|
||||
try {
|
||||
return Object.fromEntries(
|
||||
mapping.split(",").map((pair) => {
|
||||
const [key, value] = pair.split(":");
|
||||
return [key.trim(), value.trim()];
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
import { Node, Edge } from 'reactflow';
|
||||
|
||||
interface Model {
|
||||
id: string;
|
||||
name: string;
|
||||
fields: {
|
||||
name: string;
|
||||
type: string;
|
||||
defaultValue: string;
|
||||
validation: string;
|
||||
mapping?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
permissions: {
|
||||
authRequired: boolean;
|
||||
routes: string[];
|
||||
canCreateUsers?: boolean;
|
||||
canEditUsers?: boolean;
|
||||
canDeleteUsers?: boolean;
|
||||
canManageRoles?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface Route {
|
||||
id: string;
|
||||
name: string;
|
||||
method: string;
|
||||
url: string;
|
||||
flowData?: {
|
||||
nodes: any[];
|
||||
edges: any[];
|
||||
};
|
||||
}
|
||||
|
||||
interface Settings {
|
||||
globalKey: string;
|
||||
databaseType: string;
|
||||
authType: string;
|
||||
timezone: string;
|
||||
dbHost: string;
|
||||
dbPort: string;
|
||||
dbUser: string;
|
||||
dbPassword: string;
|
||||
dbName: string;
|
||||
}
|
||||
|
||||
interface FlowState {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
selectedNode: Node | null;
|
||||
models: Model[];
|
||||
roles: Role[];
|
||||
routes: Route[];
|
||||
settings: Settings;
|
||||
defaultTablesShown: boolean;
|
||||
setNodes: (nodes: Node[] | ((prev: Node[]) => Node[])) => void;
|
||||
setEdges: (edges: Edge[] | ((prev: Edge[]) => Edge[])) => void;
|
||||
setSelectedNode: (node: Node | null) => void;
|
||||
updateNodeData: (nodeId: string, newData: any) => void;
|
||||
addModel: (model: Model) => void;
|
||||
updateModel: (model: Model) => void;
|
||||
addRole: (role: Role) => void;
|
||||
updateRole: (role: Role) => void;
|
||||
deleteRole: (roleId: string) => void;
|
||||
addRoute: (route: Route) => void;
|
||||
updateRoute: (route: Route) => void;
|
||||
deleteRoute: (routeId: string) => void;
|
||||
updateSettings: (settings: Settings) => void;
|
||||
setDefaultTablesShown: (shown: boolean) => void;
|
||||
updateNode: (nodeId: string, newData: any) => void;
|
||||
}
|
||||
|
||||
const FlowContext = createContext<FlowState | undefined>(undefined);
|
||||
|
||||
export const FlowProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [nodes, setNodes] = useState<Node[]>([]);
|
||||
const [edges, setEdges] = useState<Edge[]>([]);
|
||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
||||
const [models, setModels] = useState<Model[]>([]);
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [routes, setRoutes] = useState<Route[]>([]);
|
||||
const [settings, setSettings] = useState<Settings>({
|
||||
globalKey: `key_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
databaseType: "mysql",
|
||||
authType: "session",
|
||||
timezone: "UTC",
|
||||
dbHost: "localhost",
|
||||
dbPort: "3306",
|
||||
dbUser: "root",
|
||||
dbPassword: "root",
|
||||
dbName: `database_${new Date().toISOString().split("T")[0]}`,
|
||||
});
|
||||
const [defaultTablesShown, setDefaultTablesShown] = useState<boolean>(false);
|
||||
|
||||
const updateNodeData = (nodeId: string, newData: any) => {
|
||||
setNodes((prevNodes) =>
|
||||
prevNodes.map((node) =>
|
||||
node.id === nodeId ? { ...node, data: { ...node.data, ...newData } } : node
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const addModel = (model: Model) => setModels((prev) => [...prev, model]);
|
||||
const updateModel = (model: Model) => setModels((prev) => prev.map((m) => (m.id === model.id ? model : m)));
|
||||
const addRole = (role: Role) => setRoles((prev) => [...prev, role]);
|
||||
const updateRole = (role: Role) => setRoles((prev) => prev.map((r) => (r.id === role.id ? role : r)));
|
||||
const deleteRole = (roleId: string) => setRoles((prev) => prev.filter((r) => r.id !== roleId));
|
||||
const addRoute = (route: Route) => setRoutes((prev) => [...prev, route]);
|
||||
const updateRoute = (route: Route) => setRoutes((prev) => prev.map((r) => (r.id === route.id ? route : r)));
|
||||
const deleteRoute = (routeId: string) => setRoutes((prev) => prev.filter((r) => r.id !== routeId));
|
||||
const updateNode = (nodeId: string, newData: any) => {
|
||||
console.log("Updating node in store:", nodeId, newData);
|
||||
updateNodeData(nodeId, newData);
|
||||
};
|
||||
|
||||
return (
|
||||
<FlowContext.Provider
|
||||
value={{
|
||||
nodes,
|
||||
edges,
|
||||
selectedNode,
|
||||
models,
|
||||
roles,
|
||||
routes,
|
||||
settings,
|
||||
defaultTablesShown,
|
||||
setNodes,
|
||||
setEdges,
|
||||
setSelectedNode,
|
||||
updateNodeData,
|
||||
addModel,
|
||||
updateModel,
|
||||
addRole,
|
||||
updateRole,
|
||||
deleteRole,
|
||||
addRoute,
|
||||
updateRoute,
|
||||
deleteRoute,
|
||||
updateSettings: setSettings,
|
||||
setDefaultTablesShown,
|
||||
updateNode,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FlowContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useFlowContext = () => {
|
||||
const context = useContext(FlowContext);
|
||||
if (!context) {
|
||||
throw new Error("useFlowContext must be used within a FlowProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,165 @@
|
||||
import { create } from "zustand";
|
||||
import { Node, Edge } from "reactflow";
|
||||
|
||||
interface Model {
|
||||
id: string;
|
||||
name: string;
|
||||
fields: {
|
||||
name: string;
|
||||
type: string;
|
||||
defaultValue: string;
|
||||
validation: string;
|
||||
mapping?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
permissions: {
|
||||
authRequired: boolean;
|
||||
routes: string[];
|
||||
canCreateUsers?: boolean;
|
||||
canEditUsers?: boolean;
|
||||
canDeleteUsers?: boolean;
|
||||
canManageRoles?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface Route {
|
||||
id: string;
|
||||
name: string;
|
||||
method: string;
|
||||
url: string;
|
||||
flowData?: {
|
||||
nodes: any[];
|
||||
edges: any[];
|
||||
};
|
||||
}
|
||||
|
||||
interface Settings {
|
||||
globalKey: string;
|
||||
databaseType: string;
|
||||
authType: string;
|
||||
timezone: string;
|
||||
dbHost: string;
|
||||
dbPort: string;
|
||||
dbUser: string;
|
||||
dbPassword: string;
|
||||
dbName: string;
|
||||
}
|
||||
|
||||
interface FlowState {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
selectedNode: Node | null;
|
||||
models: Model[];
|
||||
roles: Role[];
|
||||
routes: Route[];
|
||||
settings: Settings;
|
||||
defaultTablesShown: boolean;
|
||||
setNodes: (nodes: Node[] | ((prev: Node[]) => Node[])) => void;
|
||||
setEdges: (edges: Edge[] | ((prev: Edge[]) => Edge[])) => void;
|
||||
setSelectedNode: (node: Node | null) => void;
|
||||
updateNodeData: (nodeId: string, newData: any) => void;
|
||||
addModel: (model: Model) => void;
|
||||
updateModel: (model: Model) => void;
|
||||
addRole: (role: Role) => void;
|
||||
updateRole: (role: Role) => void;
|
||||
deleteRole: (roleId: string) => void;
|
||||
addRoute: (route: Route) => void;
|
||||
updateRoute: (route: Route) => void;
|
||||
deleteRoute: (routeId: string) => void;
|
||||
updateSettings: (settings: Settings) => void;
|
||||
setDefaultTablesShown: (shown: boolean) => void;
|
||||
updateNode: (nodeId: string, newData: any) => void;
|
||||
}
|
||||
|
||||
export const useFlowStore = create<FlowState>((set) => ({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
selectedNode: null,
|
||||
models: [],
|
||||
roles: [],
|
||||
routes: [],
|
||||
settings: {
|
||||
globalKey: `key_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
databaseType: "mysql",
|
||||
authType: "session",
|
||||
timezone: "UTC",
|
||||
dbHost: "localhost",
|
||||
dbPort: "3306", // normal MySQL port
|
||||
dbUser: "root",
|
||||
dbPassword: "root",
|
||||
dbName: `database_${new Date().toISOString().split("T")[0]}`, // today's date
|
||||
},
|
||||
defaultTablesShown: false,
|
||||
setNodes: (nodes) =>
|
||||
set((state) => ({
|
||||
nodes: typeof nodes === "function" ? nodes(state.nodes) : nodes,
|
||||
})),
|
||||
setEdges: (edges) =>
|
||||
set((state) => ({
|
||||
edges: typeof edges === "function" ? edges(state.edges) : edges,
|
||||
})),
|
||||
setSelectedNode: (node) => set({ selectedNode: node }),
|
||||
updateNodeData: (nodeId, newData) =>
|
||||
set((state) => ({
|
||||
nodes: state.nodes.map((node) =>
|
||||
node.id === nodeId
|
||||
? { ...node, data: { ...node.data, ...newData } }
|
||||
: node
|
||||
),
|
||||
})),
|
||||
addModel: (model) =>
|
||||
set((state) => ({
|
||||
models: [...state.models, model],
|
||||
})),
|
||||
updateModel: (model) =>
|
||||
set((state) => ({
|
||||
models: state.models.map((m) => (m.id === model.id ? model : m)),
|
||||
})),
|
||||
addRole: (role) =>
|
||||
set((state) => ({
|
||||
roles: [...state.roles, role],
|
||||
})),
|
||||
updateRole: (role) =>
|
||||
set((state) => ({
|
||||
roles: state.roles.map((r) => (r.id === role.id ? role : r)),
|
||||
})),
|
||||
deleteRole: (roleId) =>
|
||||
set((state) => ({
|
||||
roles: state.roles.filter((r) => r.id !== roleId),
|
||||
})),
|
||||
addRoute: (route) =>
|
||||
set((state) => ({
|
||||
routes: [...state.routes, route],
|
||||
})),
|
||||
updateRoute: (route) =>
|
||||
set((state) => ({
|
||||
routes: state.routes.map((r) => (r.id === route.id ? route : r)),
|
||||
})),
|
||||
deleteRoute: (routeId) =>
|
||||
set((state) => ({
|
||||
routes: state.routes.filter((r) => r.id !== routeId),
|
||||
})),
|
||||
updateSettings: (settings) => set({ settings }),
|
||||
setDefaultTablesShown: (shown) => set({ defaultTablesShown: shown }),
|
||||
updateNode: (nodeId: string, newData: any) => {
|
||||
console.log("Updating node in store:", nodeId, newData);
|
||||
set((state) => ({
|
||||
nodes: state.nodes.map((node) =>
|
||||
node.id === nodeId
|
||||
? {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
...newData,
|
||||
},
|
||||
}
|
||||
: node
|
||||
),
|
||||
}));
|
||||
},
|
||||
}));
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
optimizeDeps: {
|
||||
exclude: ['lucide-react'],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user