add in admin and member automatically generated

This commit is contained in:
ryanwong
2024-11-15 06:00:35 -05:00
parent b5d6c75de1
commit 576dff4e39
5 changed files with 473 additions and 205 deletions
+300 -121
View File
@@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from "react";
import { X, Plus, Trash } from 'lucide-react'; import { X, Plus, Trash } from "lucide-react";
import { Node } from 'reactflow'; import { Node } from "reactflow";
import { useFlowStore } from '../store/flowStore'; import { useFlowStore } from "../store/flowStore";
interface ConfigPanelProps { interface ConfigPanelProps {
node: Node | null; node: Node | null;
@@ -17,18 +17,27 @@ interface Field {
export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) { export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
const { models } = useFlowStore(); const { models } = useFlowStore();
const [newField, setNewField] = useState<Field>({ name: '', type: 'string' }); const [newField, setNewField] = useState<Field>({ name: "", type: "string" });
if (!node) return null; if (!node) return null;
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => { const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>
) => {
onUpdateNode(node.id, { onUpdateNode(node.id, {
...node.data, ...node.data,
[e.target.name]: e.target.value, [e.target.name]: e.target.value,
}); });
}; };
const handleArrayChange = (index: number, field: string, value: string, arrayName: string) => { const handleArrayChange = (
index: number,
field: string,
value: string,
arrayName: string
) => {
const array = [...(node.data[arrayName] || [])]; const array = [...(node.data[arrayName] || [])];
array[index] = { ...array[index], [field]: value }; array[index] = { ...array[index], [field]: value };
onUpdateNode(node.id, { onUpdateNode(node.id, {
@@ -43,7 +52,7 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
...node.data, ...node.data,
[arrayName]: array, [arrayName]: array,
}); });
setNewField({ name: '', type: 'string' }); setNewField({ name: "", type: "string" });
}; };
const removeField = (index: number, arrayName: string) => { const removeField = (index: number, arrayName: string) => {
@@ -55,19 +64,35 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
}); });
}; };
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 = () => ( const renderDatabaseFields = () => (
<> <>
<div className="mb-4"> <div className="mb-4">
<label className="block text-sm font-medium mb-1">Model</label> <label className="block text-sm font-medium mb-1">Model</label>
<select <select
name="model" name="model"
value={node.data.model || ''} value={node.data.model || ""}
onChange={handleChange} onChange={handleChange}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
> >
<option value="">Select Model</option> <option value="">Select Model</option>
{models.map((model) => ( {models.map((model) => (
<option key={model.id} value={model.id}>{model.name}</option> <option key={model.id} value={model.id}>
{model.name}
</option>
))} ))}
</select> </select>
</div> </div>
@@ -75,7 +100,7 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
<label className="block text-sm font-medium mb-1">SQL Query</label> <label className="block text-sm font-medium mb-1">SQL Query</label>
<textarea <textarea
name="query" name="query"
value={node.data.query || ''} value={node.data.query || ""}
onChange={handleChange} onChange={handleChange}
className="w-full p-2 border rounded h-32 font-mono text-sm" className="w-full p-2 border rounded h-32 font-mono text-sm"
placeholder="SELECT * FROM table WHERE id = :id" placeholder="SELECT * FROM table WHERE id = :id"
@@ -86,7 +111,7 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
<input <input
type="text" type="text"
name="resultVar" name="resultVar"
value={node.data.resultVar || ''} value={node.data.resultVar || ""}
onChange={handleChange} onChange={handleChange}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
placeholder="result" placeholder="result"
@@ -97,14 +122,16 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
const renderFields = () => { const renderFields = () => {
switch (node.type) { switch (node.type) {
case 'auth': case "auth":
return ( return (
<> <>
<div className="mb-4"> <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 <select
name="authType" name="authType"
value={node.data.authType || 'bearer'} value={node.data.authType || "bearer"}
onChange={handleChange} onChange={handleChange}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
> >
@@ -114,11 +141,13 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
</select> </select>
</div> </div>
<div className="mb-4"> <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 <input
type="text" type="text"
name="tokenVar" name="tokenVar"
value={node.data.tokenVar || ''} value={node.data.tokenVar || ""}
onChange={handleChange} onChange={handleChange}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
placeholder="token" placeholder="token"
@@ -127,33 +156,221 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
</> </>
); );
case 'url': case "url":
return ( return (
<> <>
<div className="mb-4"> <div className="mb-4">
<label className="block text-sm font-medium mb-1">Method</label> <label className="block text-sm font-medium mb-1">Method</label>
<select <select
name="method" name="method"
value={node.data.method || 'GET'} value={node.data.method || "GET"}
onChange={handleChange} onChange={handleChange}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
> >
{['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].map(method => ( {["GET", "POST", "PUT", "DELETE", "PATCH"].map((method) => (
<option key={method} value={method}>{method}</option> <option key={method} value={method}>
{method}
</option>
))} ))}
</select> </select>
</div> </div>
<div className="mb-4"> <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 <input
type="text" type="text"
name="path" name="path"
value={node.data.path || ''} value={node.data.path || ""}
onChange={handleChange} onChange={(e) => {
handleChange(e);
const queryParams = extractQueryParams(e.target.value);
onUpdateNode(node.id, {
...node.data,
path: e.target.value,
queryFields: queryParams,
});
}}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
placeholder="/api/users/:id" placeholder="/api/users/:id"
/> />
</div> </div>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">
Body Fields
</label>
<div className="mb-2 p-2 bg-gray-50 rounded text-sm">
{node.data.fields && node.data.fields.length > 0 ? (
node.data.fields.map((field: Field) => (
<div key={field.name} className="text-gray-600">
{field.name}: {field.type}
{field.validation ? ` (${field.validation})` : ""}
</div>
))
) : (
<div className="text-gray-500 italic">
No body fields defined
</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}
onChange={(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-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}
onChange={(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-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={() => 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 Fields
</label>
<div className="mb-2 p-2 bg-gray-50 rounded text-sm">
{node.data.queryFields && node.data.queryFields.length > 0 ? (
node.data.queryFields.map((field: Field) => (
<div key={field.name} className="text-gray-600">
{field.name}: {field.type}
{field.validation ? ` (${field.validation})` : ""}
</div>
))
) : (
<div className="text-gray-500 italic">
No query fields defined
</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}
onChange={(e) =>
handleArrayChange(
index,
"name",
e.target.value,
"queryFields"
)
}
className="flex-1 p-2 border rounded text-sm"
placeholder="Field name"
/>
<select
value={field.type}
onChange={(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={newField.name}
onChange={(e) =>
setNewField({ ...newField, name: e.target.value })
}
className="flex-1 p-2 border rounded text-sm"
placeholder="New query param"
/>
<select
value={newField.type}
onChange={(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={() => addField("queryFields")}
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"> <div className="mb-4">
<label className="block text-sm font-medium mb-1">Fields</label> <label className="block text-sm font-medium mb-1">Fields</label>
{(node.data.fields || []).map((field: Field, index: number) => ( {(node.data.fields || []).map((field: Field, index: number) => (
@@ -161,13 +378,17 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
<input <input
type="text" type="text"
value={field.name} value={field.name}
onChange={(e) => handleArrayChange(index, 'name', e.target.value, 'fields')} onChange={(e) =>
handleArrayChange(index, "name", e.target.value, "fields")
}
className="flex-1 p-2 border rounded" className="flex-1 p-2 border rounded"
placeholder="Field name" placeholder="Field name"
/> />
<select <select
value={field.type} value={field.type}
onChange={(e) => handleArrayChange(index, 'type', e.target.value, 'fields')} onChange={(e) =>
handleArrayChange(index, "type", e.target.value, "fields")
}
className="w-24 p-2 border rounded" className="w-24 p-2 border rounded"
> >
<option value="string">String</option> <option value="string">String</option>
@@ -175,15 +396,8 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
<option value="boolean">Boolean</option> <option value="boolean">Boolean</option>
<option value="date">Date</option> <option value="date">Date</option>
</select> </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 <button
onClick={() => removeField(index, 'fields')} onClick={() => removeField(index, "fields")}
className="p-2 text-red-500 hover:bg-red-50 rounded" className="p-2 text-red-500 hover:bg-red-50 rounded"
> >
<Trash className="w-4 h-4" /> <Trash className="w-4 h-4" />
@@ -194,13 +408,17 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
<input <input
type="text" type="text"
value={newField.name} value={newField.name}
onChange={(e) => setNewField({ ...newField, name: e.target.value })} onChange={(e) =>
setNewField({ ...newField, name: e.target.value })
}
className="flex-1 p-2 border rounded" className="flex-1 p-2 border rounded"
placeholder="New field name" placeholder="New field name"
/> />
<select <select
value={newField.type} value={newField.type}
onChange={(e) => setNewField({ ...newField, type: e.target.value })} onChange={(e) =>
setNewField({ ...newField, type: e.target.value })
}
className="w-24 p-2 border rounded" className="w-24 p-2 border rounded"
> >
<option value="string">String</option> <option value="string">String</option>
@@ -209,7 +427,7 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
<option value="date">Date</option> <option value="date">Date</option>
</select> </select>
<button <button
onClick={() => addField('fields')} onClick={() => addField("fields")}
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600" className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
@@ -219,76 +437,17 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
</> </>
); );
case 'output': case "variable":
return ( return (
<> <>
<div className="mb-4"> <div className="mb-4">
<label className="block text-sm font-medium mb-1">Fields</label> <label className="block text-sm font-medium mb-1">
{(node.data.fields || []).map((field: Field, index: number) => ( Variable Name
<div key={index} className="flex gap-2 mb-2"> </label>
<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"
/>
<select
value={field.type}
onChange={(e) => handleArrayChange(index, 'type', e.target.value, 'fields')}
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={() => 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">
<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>
</>
);
case 'variable':
return (
<>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Variable Name</label>
<input <input
type="text" type="text"
name="name" name="name"
value={node.data.name || ''} value={node.data.name || ""}
onChange={handleChange} onChange={handleChange}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
placeholder="myVariable" placeholder="myVariable"
@@ -298,7 +457,7 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
<label className="block text-sm font-medium mb-1">Type</label> <label className="block text-sm font-medium mb-1">Type</label>
<select <select
name="type" name="type"
value={node.data.type || 'string'} value={node.data.type || "string"}
onChange={handleChange} onChange={handleChange}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
> >
@@ -310,11 +469,13 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
</select> </select>
</div> </div>
<div className="mb-4"> <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 <input
type="text" type="text"
name="defaultValue" name="defaultValue"
value={node.data.defaultValue || ''} value={node.data.defaultValue || ""}
onChange={handleChange} onChange={handleChange}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
placeholder="Default value" placeholder="Default value"
@@ -323,13 +484,15 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
</> </>
); );
case 'logic': case "logic":
return ( return (
<div className="mb-4"> <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 <textarea
name="code" name="code"
value={node.data.code || ''} value={node.data.code || ""}
onChange={handleChange} onChange={handleChange}
className="w-full p-2 border rounded h-40 font-mono text-sm" className="w-full p-2 border rounded h-40 font-mono text-sm"
placeholder="// Write your JavaScript code here" placeholder="// Write your JavaScript code here"
@@ -337,19 +500,33 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
</div> </div>
); );
case 'db-find': case "db-find":
case 'db-query': case "db-query":
return renderDatabaseFields();
case 'db-insert':
return ( return (
<> <>
{renderDatabaseFields()} {renderDatabaseFields()}
<div className="mb-4"> <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 <textarea
name="variables" name="variables"
value={node.data.variables || ''} value={node.data.variables || ""}
onChange={handleChange} onChange={handleChange}
className="w-full p-2 border rounded h-20 font-mono text-sm" className="w-full p-2 border rounded h-20 font-mono text-sm"
placeholder="name: string&#10;age: number" placeholder="name: string&#10;age: number"
@@ -358,8 +535,8 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
</> </>
); );
case 'db-update': case "db-update":
case 'db-delete': case "db-delete":
return ( return (
<> <>
{renderDatabaseFields()} {renderDatabaseFields()}
@@ -368,18 +545,20 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
<input <input
type="text" type="text"
name="idField" name="idField"
value={node.data.idField || ''} value={node.data.idField || ""}
onChange={handleChange} onChange={handleChange}
className="w-full p-2 border rounded" className="w-full p-2 border rounded"
placeholder="id" placeholder="id"
/> />
</div> </div>
{node.type === 'db-update' && ( {node.type === "db-update" && (
<div className="mb-4"> <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 <textarea
name="variables" name="variables"
value={node.data.variables || ''} value={node.data.variables || ""}
onChange={handleChange} onChange={handleChange}
className="w-full p-2 border rounded h-20 font-mono text-sm" className="w-full p-2 border rounded h-20 font-mono text-sm"
placeholder="name: string&#10;age: number" placeholder="name: string&#10;age: number"
@@ -405,4 +584,4 @@ export function ConfigPanel({ node, onClose, onUpdateNode }: ConfigPanelProps) {
{renderFields()} {renderFields()}
</div> </div>
); );
} }
+72 -1
View File
@@ -67,7 +67,9 @@ export function DefaultTablesModal({
onClose, onClose,
}: DefaultTablesModalProps) { }: DefaultTablesModalProps) {
const [selectedTables, setSelectedTables] = useState<string[]>([]); 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) => { const handleToggleTable = (tableId: string) => {
setSelectedTables((prev) => 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(); onClose();
}; };
@@ -127,6 +165,39 @@ export function DefaultTablesModal({
<div className="mt-2 text-sm text-gray-500"> <div className="mt-2 text-sm text-gray-500">
Fields: {table.fields.map((f) => f.name).join(", ")} Fields: {table.fields.map((f) => f.name).join(", ")}
</div> </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> </div>
</label> </label>
</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, { import ReactFlow, {
Background, Background,
Controls, Controls,
@@ -9,13 +9,13 @@ import ReactFlow, {
applyNodeChanges, applyNodeChanges,
applyEdgeChanges, applyEdgeChanges,
Panel, Panel,
} from 'reactflow'; } from "reactflow";
import { ArrowLeft, Save } from 'lucide-react'; import { ArrowLeft, Save } from "lucide-react";
import { useFlowStore } from '../store/flowStore'; import { useFlowStore } from "../store/flowStore";
import { ConfigPanel } from './ConfigPanel'; import { ConfigPanel } from "./ConfigPanel";
import { ComponentsPanel } from './ComponentsPanel'; import { ComponentsPanel } from "./ComponentsPanel";
import CustomNode from './CustomNode'; import CustomNode from "./CustomNode";
import 'reactflow/dist/style.css'; import "reactflow/dist/style.css";
const nodeTypes = { const nodeTypes = {
auth: CustomNode, auth: CustomNode,
@@ -23,11 +23,11 @@ const nodeTypes = {
output: CustomNode, output: CustomNode,
logic: CustomNode, logic: CustomNode,
variable: CustomNode, variable: CustomNode,
'db-find': CustomNode, "db-find": CustomNode,
'db-insert': CustomNode, "db-insert": CustomNode,
'db-update': CustomNode, "db-update": CustomNode,
'db-delete': CustomNode, "db-delete": CustomNode,
'db-query': CustomNode, "db-query": CustomNode,
}; };
interface FlowEditorContentProps { interface FlowEditorContentProps {
@@ -55,7 +55,18 @@ function FlowEditorContent({ route, onClose }: FlowEditorContentProps) {
setNodes(route.flowData.nodes); setNodes(route.flowData.nodes);
setEdges(route.flowData.edges); setEdges(route.flowData.edges);
} else { } 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([]); setEdges([]);
} }
}, [route, setNodes, setEdges]); }, [route, setNodes, setEdges]);
@@ -77,14 +88,14 @@ function FlowEditorContent({ route, onClose }: FlowEditorContentProps) {
const onDragOver = useCallback((event: React.DragEvent) => { const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault(); event.preventDefault();
event.dataTransfer.dropEffect = 'move'; event.dataTransfer.dropEffect = "move";
}, []); }, []);
const onDrop = useCallback( const onDrop = useCallback(
(event: React.DragEvent) => { (event: React.DragEvent) => {
event.preventDefault(); event.preventDefault();
const type = event.dataTransfer.getData('application/reactflow'); const type = event.dataTransfer.getData("application/reactflow");
if (!type || !reactFlowWrapper.current) return; if (!type || !reactFlowWrapper.current) return;
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect(); const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
@@ -97,7 +108,9 @@ function FlowEditorContent({ route, onClose }: FlowEditorContentProps) {
id: `node_${Date.now()}`, id: `node_${Date.now()}`,
type, type,
position, 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]); setNodes((nds) => [...nds, newNode]);
@@ -194,4 +207,4 @@ export function RouteFlowEditor({ route, onClose }: FlowEditorContentProps) {
<FlowEditorContent route={route} onClose={onClose} /> <FlowEditorContent route={route} onClose={onClose} />
</ReactFlowProvider> </ReactFlowProvider>
); );
} }
+16 -15
View File
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import { X, Trash2 } from 'lucide-react'; import { X, Trash2 } from "lucide-react";
import { useFlowStore } from '../store/flowStore'; import { useFlowStore } from "../store/flowStore";
interface RouteModalProps { interface RouteModalProps {
isOpen: boolean; isOpen: boolean;
@@ -15,9 +15,9 @@ interface RouteModalProps {
const createInitialFormData = () => ({ const createInitialFormData = () => ({
id: `route_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, id: `route_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
name: '', name: "",
url: '', url: "",
method: 'GET', method: "GET",
}); });
export function RouteModal({ isOpen, onClose, route }: RouteModalProps) { export function RouteModal({ isOpen, onClose, route }: RouteModalProps) {
@@ -32,7 +32,9 @@ export function RouteModal({ isOpen, onClose, route }: RouteModalProps) {
} }
}, [route, isOpen]); }, [route, isOpen]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData({ setFormData({
...formData, ...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="bg-white rounded-lg w-full max-w-md">
<div className="p-4 border-b border-gray-200 flex justify-between items-center"> <div className="p-4 border-b border-gray-200 flex justify-between items-center">
<h2 className="text-lg font-semibold"> <h2 className="text-lg font-semibold">
{route ? 'Edit Route' : 'Add New Route'} {route ? "Edit Route" : "Add New Route"}
</h2> </h2>
<button <button onClick={onClose} className="p-1 hover:bg-gray-100 rounded">
onClick={onClose}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </button>
</div> </div>
@@ -76,7 +75,9 @@ export function RouteModal({ isOpen, onClose, route }: RouteModalProps) {
<div className="p-4"> <div className="p-4">
<div className="space-y-4"> <div className="space-y-4">
<div> <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 <input
type="text" type="text"
name="name" name="name"
@@ -138,11 +139,11 @@ export function RouteModal({ isOpen, onClose, route }: RouteModalProps) {
onClick={handleSave} onClick={handleSave}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
> >
{route ? 'Update' : 'Save'} Route {route ? "Update" : "Save"} Route
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
} }
+54 -50
View File
@@ -1,8 +1,8 @@
import React, { useState } from 'react'; import React, { useState } from "react";
import { Plus, Edit2, Code } from 'lucide-react'; import { Plus, Edit2, Code } from "lucide-react";
import { useFlowStore } from '../store/flowStore'; import { useFlowStore } from "../store/flowStore";
import { RouteModal } from './RouteModal'; import { RouteModal } from "./RouteModal";
import { RouteFlowEditor } from './RouteFlowEditor'; import { RouteFlowEditor } from "./RouteFlowEditor";
export function RoutesPanel() { export function RoutesPanel() {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@@ -11,55 +11,59 @@ export function RoutesPanel() {
const { routes } = useFlowStore(); const { routes } = useFlowStore();
return ( return (
<div className="space-y-4"> <div className="flex flex-col h-full">
{!editingRoute ? ( {!editingRoute ? (
<> <>
<button <div className="p-4">
onClick={() => { <button
setSelectedRoute(null); onClick={() => {
setIsModalOpen(true); 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" }}
> 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 <Plus className="w-4 h-4" />
</button> Add Route
</button>
</div>
<div className="space-y-3"> <div className="flex-1 overflow-y-auto px-4">
{routes.map((route) => ( <div className="space-y-3">
<div {routes.map((route) => (
key={route.id} <div
className="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow" key={route.id}
> className="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow"
<div className="flex justify-between items-start"> >
<div> <div className="flex justify-between items-start">
<h3 className="font-medium">{route.name}</h3> <div>
<p className="text-sm text-gray-500"> <h3 className="font-medium">{route.name}</h3>
{route.method} {route.url} <p className="text-sm text-gray-500">
</p> {route.method} {route.url}
</div> </p>
<div className="flex gap-2"> </div>
<button <div className="flex gap-2">
onClick={() => { <button
setSelectedRoute(route); onClick={() => {
setIsModalOpen(true); setSelectedRoute(route);
}} setIsModalOpen(true);
className="p-1 hover:bg-gray-100 rounded" }}
title="Edit Route" className="p-1 hover:bg-gray-100 rounded"
> title="Edit Route"
<Edit2 className="w-4 h-4" /> >
</button> <Edit2 className="w-4 h-4" />
<button </button>
onClick={() => setEditingRoute(route)} <button
className="p-1 hover:bg-gray-100 rounded" onClick={() => setEditingRoute(route)}
title="Edit Components" className="p-1 hover:bg-gray-100 rounded"
> title="Edit Components"
<Code className="w-4 h-4" /> >
</button> <Code className="w-4 h-4" />
</button>
</div>
</div> </div>
</div> </div>
</div> ))}
))} </div>
</div> </div>
<RouteModal <RouteModal
@@ -81,4 +85,4 @@ export function RoutesPanel() {
)} )}
</div> </div>
); );
} }