Compare commits

...

10 Commits

Author SHA1 Message Date
ryanwong 167e9fe6cd switch state management to context api 2024-11-18 14:11:51 -05:00
ryanwong 183d273de2 fixed the issue 2024-11-18 13:32:19 -05:00
ryanwong 2c682bcc12 set default values 2024-11-18 13:30:35 -05:00
ryanwong 07306b9a0a fixing handle change 2024-11-18 12:16:11 -05:00
ryanwong 40749d0b12 fix delete 2024-11-16 09:00:17 -05:00
ryanwong 91bd7b308e add delete 2024-11-16 08:59:03 -05:00
ryanwong 21d2a9c45e added in new crud apis made 2024-11-16 08:52:13 -05:00
ryanwong b9d1173965 still doesnt work 2024-11-15 06:22:30 -05:00
ryanwong 05895231b8 updated config panel 2024-11-15 06:12:48 -05:00
ryanwong 576dff4e39 add in admin and member automatically generated 2024-11-15 06:00:35 -05:00
10 changed files with 1111 additions and 227 deletions
+515 -137
View File
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { X, Plus, Trash } from 'lucide-react';
import { Node } from 'reactflow';
import { useFlowStore } from '../store/flowStore';
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;
@@ -15,44 +15,195 @@ interface Field {
validation?: string;
}
export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
const { models } = useFlowStore();
const [newField, setNewField] = useState<Field>({ name: '', type: 'string' });
if (!node) return null;
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
onUpdateNode(node.id, {
...node.data,
[e.target.name]: e.target.value,
});
const getDefaultDataForType = (type: string) => {
const baseData = {
label: type.charAt(0).toUpperCase() + type.slice(1).replace("-", " ")
};
const handleArrayChange = (index: number, field: string, value: string, arrayName: string) => {
const array = [...(node.data[arrayName] || [])];
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 };
onUpdateNode(node.id, {
const newData = {
...node.data,
[arrayName]: array,
});
};
onUpdateNode(node.id, newData);
updateNode(node.id, newData);
};
const addField = (arrayName: string) => {
const array = [...(node.data[arrayName] || []), newField];
onUpdateNode(node.id, {
const fieldToAdd = arrayName === "queryFields" ? newQueryField : newField;
if (!fieldToAdd.name.trim()) return;
const array = [...node.data[arrayName], { ...fieldToAdd }];
const newData = {
...node.data,
[arrayName]: array,
});
setNewField({ name: '', type: 'string' });
};
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] || [])];
const array = [...node.data[arrayName]];
array.splice(index, 1);
onUpdateNode(node.id, {
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 = () => (
@@ -61,22 +212,37 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
<label className="block text-sm font-medium mb-1">Model</label>
<select
name="model"
value={node.data.model || ''}
onChange={handleChange}
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.id}>{model.name}</option>
<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 || ''}
onChange={handleChange}
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"
/>
@@ -86,8 +252,8 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
<input
type="text"
name="resultVar"
value={node.data.resultVar || ''}
onChange={handleChange}
value={node.data.resultVar}
onBlur={handleChange}
className="w-full p-2 border rounded"
placeholder="result"
/>
@@ -97,15 +263,17 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
const renderFields = () => {
switch (node.type) {
case 'auth':
case "auth":
return (
<>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Auth Type</label>
<label className="block text-sm font-medium mb-1">
Auth Type
</label>
<select
name="authType"
value={node.data.authType || 'bearer'}
onChange={handleChange}
value={node.data.authType}
onBlur={handleChange}
className="w-full p-2 border rounded"
>
<option value="bearer">Bearer Token</option>
@@ -114,12 +282,14 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
</select>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Token Variable</label>
<label className="block text-sm font-medium mb-1">
Token Variable
</label>
<input
type="text"
name="tokenVar"
value={node.data.tokenVar || ''}
onChange={handleChange}
value={node.data.tokenVar}
onBlur={handleChange}
className="w-full p-2 border rounded"
placeholder="token"
/>
@@ -127,90 +297,210 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
</>
);
case 'url':
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 || 'GET'}
onChange={handleChange}
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>
{["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>
<label className="block text-sm font-medium mb-1">
Route Path
</label>
<input
type="text"
name="path"
value={node.data.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">Fields</label>
<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-2 mb-2">
<div key={index} className="flex gap-1 mb-2">
<input
type="text"
value={field.name}
onChange={(e) => handleArrayChange(index, 'name', e.target.value, 'fields')}
className="flex-1 p-2 border rounded"
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}
onChange={(e) => handleArrayChange(index, 'type', e.target.value, 'fields')}
className="w-24 p-2 border rounded"
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">Boolean</option>
<option value="boolean">Bool</option>
<option value="date">Date</option>
</select>
<input
type="text"
value={field.validation}
onChange={(e) => handleArrayChange(index, 'validation', e.target.value, 'fields')}
className="flex-1 p-2 border rounded"
placeholder="Validation"
/>
<button
onClick={() => removeField(index, 'fields')}
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-2 mt-2">
<div className="flex gap-1 mt-2">
<input
type="text"
value={newField.name}
onChange={(e) => setNewField({ ...newField, name: e.target.value })}
className="flex-1 p-2 border rounded"
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}
onChange={(e) => setNewField({ ...newField, type: e.target.value })}
className="w-24 p-2 border rounded"
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">Boolean</option>
<option value="boolean">Bool</option>
<option value="date">Date</option>
</select>
<button
onClick={() => addField('fields')}
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
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>
@@ -219,77 +509,143 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
</>
);
case 'output':
case "output":
return (
<>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Fields</label>
{(node.data.fields || []).map((field: Field, index: number) => (
<div key={index} className="flex gap-2 mb-2">
<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={field.name}
onChange={(e) => handleArrayChange(index, 'name', e.target.value, 'fields')}
className="flex-1 p-2 border rounded"
placeholder="Field name"
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={field.type}
onChange={(e) => handleArrayChange(index, 'type', e.target.value, 'fields')}
className="w-24 p-2 border rounded"
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={() => removeField(index, 'fields')}
className="p-2 text-red-500 hover:bg-red-50 rounded"
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"
>
<Trash className="w-4 h-4" />
<Plus className="w-4 h-4" />
</button>
</div>
))}
<div className="flex gap-2 mt-2">
<input
type="text"
value={newField.name}
onChange={(e) => setNewField({ ...newField, name: e.target.value })}
className="flex-1 p-2 border rounded"
placeholder="New field name"
/>
<select
value={newField.type}
onChange={(e) => setNewField({ ...newField, type: e.target.value })}
className="w-24 p-2 border rounded"
>
<option value="string">String</option>
<option value="number">Number</option>
<option value="boolean">Boolean</option>
<option value="date">Date</option>
</select>
<button
onClick={() => addField('fields')}
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
>
<Plus className="w-4 h-4" />
</button>
</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':
case "variable":
return (
<>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Variable Name</label>
<label className="block text-sm font-medium mb-1">
Variable Name
</label>
<input
type="text"
name="name"
value={node.data.name || ''}
onChange={handleChange}
value={node.data.name}
onBlur={handleChange}
className="w-full p-2 border rounded"
placeholder="myVariable"
/>
@@ -298,8 +654,8 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
<label className="block text-sm font-medium mb-1">Type</label>
<select
name="type"
value={node.data.type || 'string'}
onChange={handleChange}
value={node.data.type}
onBlur={handleChange}
className="w-full p-2 border rounded"
>
<option value="string">String</option>
@@ -310,12 +666,14 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
</select>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Default Value</label>
<label className="block text-sm font-medium mb-1">
Default Value
</label>
<input
type="text"
name="defaultValue"
value={node.data.defaultValue || ''}
onChange={handleChange}
value={node.data.defaultValue}
onBlur={handleChange}
className="w-full p-2 border rounded"
placeholder="Default value"
/>
@@ -323,34 +681,52 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
</>
);
case 'logic':
case "logic":
return (
<div className="mb-4">
<label className="block text-sm font-medium mb-1">JavaScript Code</label>
<label className="block text-sm font-medium mb-1">
JavaScript Code
</label>
<textarea
name="code"
value={node.data.code || ''}
onChange={handleChange}
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':
case 'db-query':
case "db-find":
return renderDatabaseFields();
case 'db-insert':
case "db-query":
return (
<>
{renderDatabaseFields()}
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Variables</label>
<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 || ''}
onChange={handleChange}
value={node.data.variables}
onBlur={handleChange}
className="w-full p-2 border rounded h-20 font-mono text-sm"
placeholder="name: string&#10;age: number"
/>
@@ -358,8 +734,8 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
</>
);
case 'db-update':
case 'db-delete':
case "db-update":
case "db-delete":
return (
<>
{renderDatabaseFields()}
@@ -368,19 +744,21 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
<input
type="text"
name="idField"
value={node.data.idField || ''}
onChange={handleChange}
value={node.data.idField}
onBlur={handleChange}
className="w-full p-2 border rounded"
placeholder="id"
/>
</div>
{node.type === 'db-update' && (
{node.type === "db-update" && (
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Variables</label>
<label className="block text-sm font-medium mb-1">
Variables
</label>
<textarea
name="variables"
value={node.data.variables || ''}
onChange={handleChange}
value={node.data.variables}
onBlur={handleChange}
className="w-full p-2 border rounded h-20 font-mono text-sm"
placeholder="name: string&#10;age: number"
/>
@@ -405,4 +783,4 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
{renderFields()}
</div>
);
}
}
+72 -1
View File
@@ -67,7 +67,9 @@ export function DefaultTablesModal({
onClose,
}: DefaultTablesModalProps) {
const [selectedTables, setSelectedTables] = useState<string[]>([]);
const { addModel } = useFlowStore();
const [includeAdminRole, setIncludeAdminRole] = useState(true);
const [includeMemberRole, setIncludeMemberRole] = useState(true);
const { addModel, addRole } = useFlowStore();
const handleToggleTable = (tableId: string) => {
setSelectedTables((prev) =>
@@ -88,6 +90,42 @@ export function DefaultTablesModal({
});
}
});
// 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();
};
@@ -127,6 +165,39 @@ export function DefaultTablesModal({
<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>
+210 -3
View File
@@ -80,10 +80,15 @@ const createInitialModelData = () => ({
});
export function ModelModal({ isOpen, onClose, model }: ModelModalProps) {
const [modelData, setModelData] = useState(createInitialModelData());
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 } = useFlowStore();
const { addModel, updateModel, addRoute } = useFlowStore();
useEffect(() => {
if (model) {
@@ -122,8 +127,198 @@ export function ModelModal({ isOpen, onClose, model }: ModelModalProps) {
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();
}
onClose();
}
};
@@ -327,6 +522,18 @@ export function ModelModal({ isOpen, onClose, model }: ModelModalProps) {
<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>
+31 -18
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useRef, useEffect } from 'react';
import React, { useCallback, useRef, useEffect } from "react";
import ReactFlow, {
Background,
Controls,
@@ -9,13 +9,13 @@ import ReactFlow, {
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';
} 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,
@@ -23,11 +23,11 @@ const nodeTypes = {
output: CustomNode,
logic: CustomNode,
variable: CustomNode,
'db-find': CustomNode,
'db-insert': CustomNode,
'db-update': CustomNode,
'db-delete': CustomNode,
'db-query': CustomNode,
"db-find": CustomNode,
"db-insert": CustomNode,
"db-update": CustomNode,
"db-delete": CustomNode,
"db-query": CustomNode,
};
interface FlowEditorContentProps {
@@ -55,7 +55,18 @@ function FlowEditorContent({ route, onClose }: FlowEditorContentProps) {
setNodes(route.flowData.nodes);
setEdges(route.flowData.edges);
} else {
setNodes([]);
// 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]);
@@ -77,14 +88,14 @@ function FlowEditorContent({ route, onClose }: FlowEditorContentProps) {
const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
event.dataTransfer.dropEffect = "move";
}, []);
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData('application/reactflow');
const type = event.dataTransfer.getData("application/reactflow");
if (!type || !reactFlowWrapper.current) return;
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
@@ -97,7 +108,9 @@ function FlowEditorContent({ route, onClose }: FlowEditorContentProps) {
id: `node_${Date.now()}`,
type,
position,
data: { label: type.charAt(0).toUpperCase() + type.slice(1).replace('-', ' ') },
data: {
label: type.charAt(0).toUpperCase() + type.slice(1).replace("-", " "),
},
};
setNodes((nds) => [...nds, newNode]);
@@ -194,4 +207,4 @@ export function RouteFlowEditor({ route, onClose }: FlowEditorContentProps) {
<FlowEditorContent route={route} onClose={onClose} />
</ReactFlowProvider>
);
}
}
+16 -15
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { X, Trash2 } from 'lucide-react';
import { useFlowStore } from '../store/flowStore';
import React, { useState, useEffect } from "react";
import { X, Trash2 } from "lucide-react";
import { useFlowStore } from "../store/flowStore";
interface RouteModalProps {
isOpen: boolean;
@@ -15,9 +15,9 @@ interface RouteModalProps {
const createInitialFormData = () => ({
id: `route_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name: '',
url: '',
method: 'GET',
name: "",
url: "",
method: "GET",
});
export function RouteModal({ isOpen, onClose, route }: RouteModalProps) {
@@ -32,7 +32,9 @@ export function RouteModal({ isOpen, onClose, route }: RouteModalProps) {
}
}, [route, isOpen]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setFormData({
...formData,
@@ -63,12 +65,9 @@ export function RouteModal({ isOpen, onClose, route }: RouteModalProps) {
<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'}
{route ? "Edit Route" : "Add New Route"}
</h2>
<button
onClick={onClose}
className="p-1 hover:bg-gray-100 rounded"
>
<button onClick={onClose} className="p-1 hover:bg-gray-100 rounded">
<X className="w-5 h-5" />
</button>
</div>
@@ -76,7 +75,9 @@ export function RouteModal({ isOpen, onClose, route }: RouteModalProps) {
<div className="p-4">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Route Name</label>
<label className="block text-sm font-medium mb-1">
Route Name
</label>
<input
type="text"
name="name"
@@ -138,11 +139,11 @@ export function RouteModal({ isOpen, onClose, route }: RouteModalProps) {
onClick={handleSave}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
{route ? 'Update' : 'Save'} Route
{route ? "Update" : "Save"} Route
</button>
</div>
</div>
</div>
</div>
);
}
}
+54 -50
View File
@@ -1,8 +1,8 @@
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';
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);
@@ -11,55 +11,59 @@ export function RoutesPanel() {
const { routes } = useFlowStore();
return (
<div className="space-y-4">
<div className="flex flex-col h-full">
{!editingRoute ? (
<>
<button
onClick={() => {
setSelectedRoute(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 Route
</button>
<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="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 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>
</div>
<RouteModal
@@ -81,4 +85,4 @@ export function RoutesPanel() {
)}
</div>
);
}
}
+19
View File
@@ -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;
+13
View File
@@ -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')
);
+161
View File
@@ -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;
};
+20 -3
View File
@@ -30,11 +30,11 @@ interface Role {
interface Route {
id: string;
name: string;
url: string;
method: string;
url: string;
flowData?: {
nodes: Node[];
edges: Edge[];
nodes: any[];
edges: any[];
};
}
@@ -73,6 +73,7 @@ interface FlowState {
deleteRoute: (routeId: string) => void;
updateSettings: (settings: Settings) => void;
setDefaultTablesShown: (shown: boolean) => void;
updateNode: (nodeId: string, newData: any) => void;
}
export const useFlowStore = create<FlowState>((set) => ({
@@ -145,4 +146,20 @@ export const useFlowStore = create<FlowState>((set) => ({
})),
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
),
}));
},
}));