fix exporting models
This commit is contained in:
+130
-33
@@ -1,12 +1,20 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { X, Plus, Trash } from 'lucide-react';
|
import { X, Plus, Trash } from "lucide-react";
|
||||||
import { useFlowStore } from '../store/flowStore';
|
import { useFlowStore } from "../store/flowStore";
|
||||||
|
|
||||||
interface Field {
|
interface Field {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
validation: string;
|
validation: string;
|
||||||
|
validationOptions?: {
|
||||||
|
pattern?: string;
|
||||||
|
enum?: string[];
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
minLength?: number;
|
||||||
|
maxLength?: number;
|
||||||
|
};
|
||||||
mapping?: string;
|
mapping?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,35 +29,53 @@ interface ModelModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fieldTypes = [
|
const fieldTypes = [
|
||||||
'string',
|
"primary key",
|
||||||
'number',
|
"string",
|
||||||
'boolean',
|
"long text",
|
||||||
'date',
|
"integer",
|
||||||
'mapping',
|
"double",
|
||||||
'array',
|
"big number",
|
||||||
'object',
|
"boolean",
|
||||||
|
"date",
|
||||||
|
"datetime",
|
||||||
|
"uuid",
|
||||||
|
"json",
|
||||||
|
"mapping",
|
||||||
];
|
];
|
||||||
|
|
||||||
const validationRules = [
|
const validationRules = [
|
||||||
'required',
|
"required",
|
||||||
'email',
|
"email",
|
||||||
'url',
|
"url",
|
||||||
'min',
|
"min",
|
||||||
'max',
|
"max",
|
||||||
'pattern',
|
"pattern",
|
||||||
'enum',
|
"enum",
|
||||||
|
"length",
|
||||||
|
"minLength",
|
||||||
|
"maxLength",
|
||||||
|
"positive",
|
||||||
|
"negative",
|
||||||
|
"integer",
|
||||||
|
"decimal",
|
||||||
|
"alphanumeric",
|
||||||
|
"uuid",
|
||||||
|
"json",
|
||||||
|
"date",
|
||||||
|
"phone",
|
||||||
];
|
];
|
||||||
|
|
||||||
const initialNewField = {
|
const initialNewField: Field = {
|
||||||
name: '',
|
name: "",
|
||||||
type: 'string',
|
type: "string",
|
||||||
defaultValue: '',
|
defaultValue: "",
|
||||||
validation: '',
|
validation: "",
|
||||||
|
validationOptions: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const createInitialModelData = () => ({
|
const createInitialModelData = () => ({
|
||||||
id: `model_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
id: `model_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||||
name: '',
|
name: "",
|
||||||
fields: [],
|
fields: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,12 +134,9 @@ export function ModelModal({ isOpen, onClose, model }: ModelModalProps) {
|
|||||||
<div className="bg-white rounded-lg w-full max-w-2xl max-h-[80vh] overflow-hidden">
|
<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">
|
<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">
|
||||||
{model ? 'Edit Model' : 'Add New Model'}
|
{model ? "Edit Model" : "Add New Model"}
|
||||||
</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>
|
||||||
@@ -124,7 +147,9 @@ export function ModelModal({ isOpen, onClose, model }: ModelModalProps) {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={modelData.name}
|
value={modelData.name}
|
||||||
onChange={(e) => setModelData({ ...modelData, name: e.target.value })}
|
onChange={(e) =>
|
||||||
|
setModelData({ ...modelData, name: e.target.value })
|
||||||
|
}
|
||||||
className="w-full p-2 border rounded"
|
className="w-full p-2 border rounded"
|
||||||
placeholder="Enter model name"
|
placeholder="Enter model name"
|
||||||
/>
|
/>
|
||||||
@@ -211,10 +236,10 @@ export function ModelModal({ isOpen, onClose, model }: ModelModalProps) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{newField.type === 'mapping' && (
|
{newField.type === "mapping" && (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newField.mapping || ''}
|
value={newField.mapping || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewField({ ...newField, mapping: e.target.value })
|
setNewField({ ...newField, mapping: e.target.value })
|
||||||
}
|
}
|
||||||
@@ -223,6 +248,78 @@ export function ModelModal({ isOpen, onClose, model }: ModelModalProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{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
|
<button
|
||||||
onClick={handleAddField}
|
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"
|
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"
|
||||||
@@ -245,10 +342,10 @@ export function ModelModal({ isOpen, onClose, model }: ModelModalProps) {
|
|||||||
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"
|
||||||
>
|
>
|
||||||
{model ? 'Update' : 'Save'} Model
|
{model ? "Update" : "Save"} Model
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
import { Plus, Edit2 } from 'lucide-react';
|
import { Plus, Edit2 } from "lucide-react";
|
||||||
import { useFlowStore } from '../store/flowStore';
|
import { useFlowStore } from "../store/flowStore";
|
||||||
import { ModelModal } from './ModelModal';
|
import { ModelModal } from "./ModelModal";
|
||||||
|
|
||||||
export function ModelPanel() {
|
export function ModelPanel() {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
@@ -36,10 +36,11 @@ export function ModelPanel() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium">{model.name}</h3>
|
<h3 className="font-medium">{model.name}</h3>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{model.fields.length} field{model.fields.length !== 1 ? 's' : ''}
|
{model.fields.length} field
|
||||||
|
{model.fields.length !== 1 ? "s" : ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="p-1 hover:bg-gray-100 rounded"
|
className="p-1 hover:bg-gray-100 rounded"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -63,4 +64,4 @@ export function ModelPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Save } from 'lucide-react';
|
import { Save } from "lucide-react";
|
||||||
import { useFlowStore } from '../store/flowStore';
|
import { useFlowStore } from "../store/flowStore";
|
||||||
|
import { TranslationService } from "../services/TranslationService";
|
||||||
|
|
||||||
export function SettingsForm() {
|
export function SettingsForm() {
|
||||||
const { settings, updateSettings } = useFlowStore();
|
const { settings, updateSettings } = useFlowStore();
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||||
|
) => {
|
||||||
updateSettings({
|
updateSettings({
|
||||||
...settings,
|
...settings,
|
||||||
[e.target.name]: e.target.value,
|
[e.target.name]: e.target.value,
|
||||||
@@ -16,6 +19,25 @@ export function SettingsForm() {
|
|||||||
updateSettings(settings);
|
updateSettings(settings);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExportConfiguration = () => {
|
||||||
|
const models = useFlowStore.getState().models;
|
||||||
|
const configuration = {
|
||||||
|
models: models.map((model) => TranslationService.translateModel(model)),
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = "model-configuration.json";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -23,7 +45,7 @@ export function SettingsForm() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="globalKey"
|
name="globalKey"
|
||||||
value={settings?.globalKey || ''}
|
value={settings?.globalKey || ""}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full p-2 border rounded"
|
className="w-full p-2 border rounded"
|
||||||
placeholder="Enter global key"
|
placeholder="Enter global key"
|
||||||
@@ -34,7 +56,7 @@ export function SettingsForm() {
|
|||||||
<label className="block text-sm font-medium mb-1">Database Type</label>
|
<label className="block text-sm font-medium mb-1">Database Type</label>
|
||||||
<select
|
<select
|
||||||
name="databaseType"
|
name="databaseType"
|
||||||
value={settings?.databaseType || ''}
|
value={settings?.databaseType || ""}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full p-2 border rounded"
|
className="w-full p-2 border rounded"
|
||||||
>
|
>
|
||||||
@@ -47,10 +69,12 @@ export function SettingsForm() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Authentication Type</label>
|
<label className="block text-sm font-medium mb-1">
|
||||||
|
Authentication Type
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
name="authType"
|
name="authType"
|
||||||
value={settings?.authType || ''}
|
value={settings?.authType || ""}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full p-2 border rounded"
|
className="w-full p-2 border rounded"
|
||||||
>
|
>
|
||||||
@@ -63,7 +87,7 @@ export function SettingsForm() {
|
|||||||
<label className="block text-sm font-medium mb-1">Timezone</label>
|
<label className="block text-sm font-medium mb-1">Timezone</label>
|
||||||
<select
|
<select
|
||||||
name="timezone"
|
name="timezone"
|
||||||
value={settings?.timezone || ''}
|
value={settings?.timezone || ""}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full p-2 border rounded"
|
className="w-full p-2 border rounded"
|
||||||
>
|
>
|
||||||
@@ -75,13 +99,15 @@ export function SettingsForm() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="block text-sm font-medium">Database Credentials</label>
|
<label className="block text-sm font-medium">
|
||||||
|
Database Credentials
|
||||||
|
</label>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="dbHost"
|
name="dbHost"
|
||||||
value={settings?.dbHost || ''}
|
value={settings?.dbHost || ""}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full p-2 border rounded mb-2"
|
className="w-full p-2 border rounded mb-2"
|
||||||
placeholder="Host"
|
placeholder="Host"
|
||||||
@@ -92,7 +118,7 @@ export function SettingsForm() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="dbPort"
|
name="dbPort"
|
||||||
value={settings?.dbPort || ''}
|
value={settings?.dbPort || ""}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full p-2 border rounded mb-2"
|
className="w-full p-2 border rounded mb-2"
|
||||||
placeholder="Port"
|
placeholder="Port"
|
||||||
@@ -103,7 +129,7 @@ export function SettingsForm() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="dbUser"
|
name="dbUser"
|
||||||
value={settings?.dbUser || ''}
|
value={settings?.dbUser || ""}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full p-2 border rounded mb-2"
|
className="w-full p-2 border rounded mb-2"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
@@ -114,7 +140,7 @@ export function SettingsForm() {
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="dbPassword"
|
name="dbPassword"
|
||||||
value={settings?.dbPassword || ''}
|
value={settings?.dbPassword || ""}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full p-2 border rounded mb-2"
|
className="w-full p-2 border rounded mb-2"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
@@ -125,7 +151,7 @@ export function SettingsForm() {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="dbName"
|
name="dbName"
|
||||||
value={settings?.dbName || ''}
|
value={settings?.dbName || ""}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full p-2 border rounded"
|
className="w-full p-2 border rounded"
|
||||||
placeholder="Database Name"
|
placeholder="Database Name"
|
||||||
@@ -140,6 +166,15 @@ export function SettingsForm() {
|
|||||||
<Save className="w-4 h-4" />
|
<Save className="w-4 h-4" />
|
||||||
Save Settings
|
Save Settings
|
||||||
</button>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user