289 lines
9.9 KiB
TypeScript
289 lines
9.9 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|
import { useComponents } from '../../contexts/ComponentContext'
|
|
import { FaRegSave, FaSync, FaCog } from 'react-icons/fa'
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
import { Formik, Form, Field } from 'formik'
|
|
import * as Yup from 'yup'
|
|
import { Button, Checkbox, FormContainer, FormItem, Input } from '@/components/ui'
|
|
import Dialog from '@/components/ui/Dialog'
|
|
import { generateDesignerCode } from '@/components/visualDesigner/codeGenerator'
|
|
import {
|
|
createDesignerId,
|
|
createEmptyDesignerDocument,
|
|
type DesignerDocument,
|
|
} from '@/components/visualDesigner/types'
|
|
|
|
// Validation schema
|
|
const validationSchema = Yup.object({
|
|
name: Yup.string().required(),
|
|
routePath: Yup.string().required().matches(/^\//, 'Route path must start with /'),
|
|
description: Yup.string(),
|
|
dependencies: Yup.array().of(Yup.string()),
|
|
isActive: Yup.boolean(),
|
|
})
|
|
|
|
interface ComponentEditorProps {
|
|
componentId?: string | null
|
|
onClose?: () => void
|
|
onSaved?: () => void | Promise<void>
|
|
}
|
|
|
|
const ComponentEditor: React.FC<ComponentEditorProps> = ({
|
|
componentId,
|
|
onClose,
|
|
onSaved,
|
|
}) => {
|
|
const id = componentId || undefined
|
|
const { translate } = useLocalization()
|
|
|
|
const { getComponent, addComponent, updateComponent, refreshComponent } = useComponents()
|
|
|
|
const [isLoaded, setIsLoaded] = useState(false)
|
|
|
|
const isEditing = !!id
|
|
|
|
// Initial values for Formik
|
|
const [initialValues, setInitialValues] = useState({
|
|
name: '',
|
|
routePath: '',
|
|
description: '',
|
|
dependencies: [] as string[],
|
|
isActive: true,
|
|
})
|
|
|
|
// Load existing component data - sadece edit modunda
|
|
useEffect(() => {
|
|
if (isEditing && id && !isLoaded) {
|
|
let cancelled = false
|
|
|
|
void refreshComponent(id).then((freshComponent) => {
|
|
if (cancelled) return
|
|
const component = freshComponent
|
|
if (!component) {
|
|
setIsLoaded(true)
|
|
return
|
|
}
|
|
|
|
// Parse dependencies from JSON string
|
|
let deps: string[] = []
|
|
try {
|
|
deps = component.dependencies ? JSON.parse(component.dependencies) : []
|
|
deps = Array.isArray(deps) ? deps : []
|
|
} catch {
|
|
deps = []
|
|
}
|
|
|
|
const values = {
|
|
name: component.name,
|
|
routePath: component.routePath,
|
|
description: component.description || '',
|
|
dependencies: deps,
|
|
isActive: component.isActive,
|
|
}
|
|
|
|
setInitialValues(values)
|
|
setIsLoaded(true)
|
|
})
|
|
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
} else if (!isEditing && !isLoaded) {
|
|
// Yeni komponent için boş başla - TEMPLATE YOK
|
|
setIsLoaded(true)
|
|
}
|
|
}, [id, isEditing, isLoaded, refreshComponent])
|
|
|
|
const createDefaultVisualDocument = (componentName: string): DesignerDocument => {
|
|
const document = createEmptyDesignerDocument('visual')
|
|
document.nodes = [
|
|
{
|
|
id: createDesignerId(),
|
|
type: 'span',
|
|
kind: 'html',
|
|
props: { children: componentName.trim() },
|
|
events: {},
|
|
bindings: {},
|
|
children: [],
|
|
},
|
|
]
|
|
return document
|
|
}
|
|
|
|
const handleSubmit = async (values: typeof initialValues, { setSubmitting }: any) => {
|
|
if (!values.name.trim()) {
|
|
alert('Please enter a component name')
|
|
setSubmitting(false)
|
|
return
|
|
}
|
|
|
|
try {
|
|
const existingComponent = isEditing && id ? getComponent(id) : undefined
|
|
const defaultDocument = createDefaultVisualDocument(values.name)
|
|
const componentData = {
|
|
name: values.name.trim(),
|
|
routePath: values.routePath.trim(),
|
|
description: values.description.trim(),
|
|
dependencies: JSON.stringify(values.dependencies), // Serialize dependencies to JSON string
|
|
code: existingComponent
|
|
? existingComponent.code
|
|
: generateDesignerCode(values.name, defaultDocument),
|
|
props: existingComponent
|
|
? existingComponent.props
|
|
: JSON.stringify({ visualDesigner: defaultDocument }),
|
|
isActive: values.isActive,
|
|
}
|
|
|
|
if (isEditing && id) {
|
|
await updateComponent(id, componentData)
|
|
} else {
|
|
await addComponent(componentData)
|
|
}
|
|
|
|
await onSaved?.()
|
|
onClose?.()
|
|
} catch (error) {
|
|
console.error('Error saving component:', error)
|
|
alert('Failed to save component. Please try again.')
|
|
} finally {
|
|
setSubmitting(false)
|
|
}
|
|
}
|
|
|
|
// Loading state - komponent yüklenene kadar bekle
|
|
if (isEditing && !isLoaded) {
|
|
return (
|
|
<div className="flex min-h-72 items-center justify-center">
|
|
<div className="text-center">
|
|
<FaSync className="w-8 h-8 text-blue-500 animate-spin mx-auto mb-3" />
|
|
<p className="text-slate-600">{translate('::App.Platform.LoadingWithThreeDot')}</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Formik
|
|
enableReinitialize
|
|
initialValues={initialValues}
|
|
validationSchema={validationSchema}
|
|
onSubmit={handleSubmit}
|
|
>
|
|
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => {
|
|
return (
|
|
<>
|
|
<Form className="pb-3 pt-8">
|
|
<div className="space-y-3">
|
|
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-slate-200 dark:border-gray-700 p-3">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<div className="bg-blue-100 dark:bg-blue-900/20 p-1.5 rounded-lg">
|
|
<FaCog className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
|
</div>
|
|
<h2 className="text-base font-semibold text-slate-900 dark:text-gray-100">
|
|
Component Settings
|
|
</h2>
|
|
</div>
|
|
|
|
<FormContainer size="sm">
|
|
<FormItem
|
|
label={translate('::App.DeveloperKitComponentEditor.ComponentName')}
|
|
invalid={!!(errors.name && touched.name)}
|
|
errorMessage={errors.name as string}
|
|
>
|
|
<Field
|
|
autoFocus
|
|
name="name"
|
|
type="text"
|
|
component={Input}
|
|
placeholder="e.g., Button, Card, Modal"
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setFieldValue('name', e.target.value)
|
|
}}
|
|
/>
|
|
</FormItem>
|
|
|
|
<FormItem
|
|
label="Route Path"
|
|
invalid={!!(errors.routePath && touched.routePath)}
|
|
errorMessage={errors.routePath as string}
|
|
>
|
|
<Field
|
|
name="routePath"
|
|
type="text"
|
|
component={Input}
|
|
placeholder="e.g., /roles or /admin/reports"
|
|
/>
|
|
</FormItem>
|
|
|
|
<FormItem
|
|
label={translate('::App.Listform.ListformField.Description')}
|
|
invalid={!!(errors.description && touched.description)}
|
|
errorMessage={errors.description as string}
|
|
>
|
|
<Field
|
|
name="description"
|
|
type="text"
|
|
component={Input}
|
|
placeholder="Brief description of the component"
|
|
textArea={true}
|
|
/>
|
|
</FormItem>
|
|
|
|
<FormItem
|
|
label={translate('::App.DeveloperKitComponentEditor.Dependencies')}
|
|
invalid={!!(errors.dependencies && touched.dependencies)}
|
|
errorMessage={errors.dependencies as string}
|
|
>
|
|
<Field name="dependencies">
|
|
{() => (
|
|
<Input
|
|
type="text"
|
|
value={(values.dependencies || []).join(', ')}
|
|
placeholder="MyComponent, AnotherComponent, etc."
|
|
onChange={(e) =>
|
|
setFieldValue(
|
|
'dependencies',
|
|
e.target.value
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean),
|
|
)
|
|
}
|
|
/>
|
|
)}
|
|
</Field>
|
|
</FormItem>
|
|
|
|
<FormItem label={translate('::App.Listform.ListformField.IsActive')}>
|
|
<Field name="isActive" component={Checkbox} />
|
|
</FormItem>
|
|
</FormContainer>
|
|
</div>
|
|
</div>
|
|
</Form>
|
|
<Dialog.Footer className="flex flex-col items-stretch gap-2 border-t border-slate-200 pt-3 dark:border-slate-700">
|
|
<div className="flex items-center justify-end gap-2">
|
|
<Button type="button" size="sm" disabled={isSubmitting} onClick={onClose}>
|
|
{translate('::App.Platform.Cancel')}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="solid"
|
|
size="sm"
|
|
disabled={isSubmitting || !values.name.trim() || !isValid}
|
|
className="flex items-center gap-2"
|
|
onClick={submitForm}
|
|
>
|
|
<FaRegSave className="h-4 w-4" />
|
|
{isSubmitting ? translate('::App.Platform.Saving') : translate('::App.Platform.Save')}
|
|
</Button>
|
|
</div>
|
|
</Dialog.Footer>
|
|
</>
|
|
)
|
|
}}
|
|
</Formik>
|
|
)
|
|
}
|
|
|
|
export default ComponentEditor
|