sozsoft-platform/ui/src/views/developerKit/ComponentEditor.tsx

290 lines
9.9 KiB
TypeScript
Raw Normal View History

2026-07-06 09:44:26 +00:00
import React, { useState, useEffect } from 'react'
2026-02-24 20:44:16 +00:00
import { useComponents } from '../../contexts/ComponentContext'
2026-08-06 13:17:59 +00:00
import { FaRegSave, FaSync, FaCog } from 'react-icons/fa'
2026-02-24 20:44:16 +00:00
import { useLocalization } from '@/utils/hooks/useLocalization'
2026-07-06 09:44:26 +00:00
import { Formik, Form, Field } from 'formik'
2026-02-24 20:44:16 +00:00
import * as Yup from 'yup'
import { Button, Checkbox, FormContainer, FormItem, Input } from '@/components/ui'
2026-08-06 13:17:59 +00:00
import Dialog from '@/components/ui/Dialog'
2026-08-05 20:51:43 +00:00
import { generateDesignerCode } from '@/components/visualDesigner/codeGenerator'
import {
createDesignerId,
createEmptyDesignerDocument,
type DesignerDocument,
} from '@/components/visualDesigner/types'
2026-02-24 20:44:16 +00:00
// Validation schema
const validationSchema = Yup.object({
name: Yup.string().required(),
2026-07-11 20:35:32 +00:00
routePath: Yup.string().required().matches(/^\//, 'Route path must start with /'),
2026-02-24 20:44:16 +00:00
description: Yup.string(),
dependencies: Yup.array().of(Yup.string()),
isActive: Yup.boolean(),
})
2026-08-06 13:17:59 +00:00
interface ComponentEditorProps {
componentId?: string | null
onClose?: () => void
onSaved?: () => void | Promise<void>
}
const ComponentEditor: React.FC<ComponentEditorProps> = ({
componentId,
onClose,
onSaved,
}) => {
const id = componentId || undefined
2026-02-24 20:44:16 +00:00
const { translate } = useLocalization()
const { getComponent, addComponent, updateComponent, refreshComponent } = useComponents()
2026-02-24 20:44:16 +00:00
const [isLoaded, setIsLoaded] = useState(false)
const isEditing = !!id
// Initial values for Formik
const [initialValues, setInitialValues] = useState({
name: '',
2026-07-11 20:35:32 +00:00
routePath: '',
2026-02-24 20:44:16 +00:00
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
}
2026-02-24 20:44:16 +00:00
// 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,
2026-07-11 20:35:32 +00:00
routePath: component.routePath,
2026-02-24 20:44:16 +00:00
description: component.description || '',
dependencies: deps,
isActive: component.isActive,
}
setInitialValues(values)
setIsLoaded(true)
})
return () => {
cancelled = true
2026-02-24 20:44:16 +00:00
}
} else if (!isEditing && !isLoaded) {
// Yeni komponent için boş başla - TEMPLATE YOK
setIsLoaded(true)
}
}, [id, isEditing, isLoaded, refreshComponent])
2026-02-24 20:44:16 +00:00
2026-08-05 20:51:43 +00:00
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
2026-02-24 20:44:16 +00:00
}
const handleSubmit = async (values: typeof initialValues, { setSubmitting }: any) => {
if (!values.name.trim()) {
alert('Please enter a component name')
setSubmitting(false)
return
}
try {
2026-08-05 20:51:43 +00:00
const existingComponent = isEditing && id ? getComponent(id) : undefined
const defaultDocument = createDefaultVisualDocument(values.name)
2026-02-24 20:44:16 +00:00
const componentData = {
name: values.name.trim(),
2026-07-11 20:35:32 +00:00
routePath: values.routePath.trim(),
2026-02-24 20:44:16 +00:00
description: values.description.trim(),
dependencies: JSON.stringify(values.dependencies), // Serialize dependencies to JSON string
2026-08-05 20:51:43 +00:00
code: existingComponent
? existingComponent.code
: generateDesignerCode(values.name, defaultDocument),
props: existingComponent
? existingComponent.props
: JSON.stringify({ visualDesigner: defaultDocument }),
2026-02-24 20:44:16 +00:00
isActive: values.isActive,
}
if (isEditing && id) {
2026-07-11 20:35:32 +00:00
await updateComponent(id, componentData)
2026-02-24 20:44:16 +00:00
} else {
2026-07-11 20:35:32 +00:00
await addComponent(componentData)
2026-02-24 20:44:16 +00:00
}
2026-08-06 13:17:59 +00:00
await onSaved?.()
onClose?.()
2026-02-24 20:44:16 +00:00
} 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 (
2026-08-06 13:17:59 +00:00
<div className="flex min-h-72 items-center justify-center">
2026-02-24 20:44:16 +00:00
<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>
2026-02-24 20:44:16 +00:00
</div>
</div>
)
}
return (
<Formik
enableReinitialize
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => {
return (
<>
2026-08-06 13:17:59 +00:00
<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>
2026-02-24 20:44:16 +00:00
</div>
<FormContainer size="sm">
<FormItem
label={translate('::App.DeveloperKitComponentEditor.ComponentName')}
invalid={!!(errors.name && touched.name)}
errorMessage={errors.name as string}
>
<Field
2026-07-06 09:44:26 +00:00
autoFocus
name="name"
type="text"
component={Input}
placeholder="e.g., Button, Card, Modal"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
2026-08-05 20:51:43 +00:00
setFieldValue('name', e.target.value)
2026-07-06 09:44:26 +00:00
}}
/>
</FormItem>
2026-07-11 20:35:32 +00:00
<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}
2026-07-06 09:44:26 +00:00
placeholder="Brief description of the component"
2026-08-06 13:17:59 +00:00
textArea={true}
2026-07-06 09:44:26 +00:00
/>
</FormItem>
<FormItem
label={translate('::App.DeveloperKitComponentEditor.Dependencies')}
invalid={!!(errors.dependencies && touched.dependencies)}
errorMessage={errors.dependencies as string}
>
<Field name="dependencies">
2026-07-06 09:44:26 +00:00
{() => (
<Input
type="text"
value={(values.dependencies || []).join(', ')}
2026-07-06 09:44:26 +00:00
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>
2026-02-24 20:44:16 +00:00
</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>
2026-08-06 13:17:59 +00:00
</Dialog.Footer>
</>
)
}}
2026-02-24 20:44:16 +00:00
</Formik>
)
}
export default ComponentEditor