sozsoft-platform/ui/src/views/developerKit/ComponentEditor.tsx
2026-06-26 01:55:44 +03:00

428 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useCallback } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'
import { useComponents } from '../../contexts/ComponentContext'
import {
FaRegSave,
FaArrowLeft,
FaExclamationCircle,
FaSync,
FaCode,
FaEye,
FaCog,
} from 'react-icons/fa'
import { parseReactCode } from '../../utils/codeParser'
import ComponentPreview from '../../components/componentEditor/ComponentPreview'
import { EditorState } from '../../proxy/developerKit/componentInfo'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps } from 'formik'
import * as Yup from 'yup'
import { Button, Checkbox, FormContainer, FormItem, Input } from '@/components/ui'
// Error tipini tanımla
interface ValidationError {
message: string
severity: number
startLineNumber?: number
}
// Validation schema
const validationSchema = Yup.object({
name: Yup.string().required(),
description: Yup.string(),
dependencies: Yup.array().of(Yup.string()),
code: Yup.string(),
isActive: Yup.boolean(),
})
const ComponentEditor: React.FC = () => {
const { id } = useParams()
const navigate = useNavigate()
const { translate } = useLocalization()
const { getComponent, addComponent, updateComponent } = useComponents()
const [validationErrors, setValidationErrors] = useState<ValidationError[]>([])
const [isLoaded, setIsLoaded] = useState(false)
const isEditing = !!id
//Eksikler Dahil edit modunda
const [editorState, setEditorState] = useState<EditorState>({
code: '',
components: [],
selectedComponentId: null,
})
// Initial values for Formik
const [initialValues, setInitialValues] = useState({
name: '',
description: '',
dependencies: [] as string[],
code: '',
isActive: true,
})
const parseAndUpdateComponents = useCallback((code: string) => {
try {
const parsed = parseReactCode(code)
setEditorState((prev) => ({
...prev,
code,
components: parsed.components,
}))
} catch (error) {
const msg = error instanceof Error ? error.message : 'Koddan komponentler parse edilemedi.'
console.log('Parse error:', msg)
setEditorState((prev) => ({
...prev,
code, // Only update the code, keep existing components
}))
}
}, [])
// Load existing component data - sadece edit modunda
useEffect(() => {
if (isEditing && id && !isLoaded) {
const component = getComponent(id)
if (component) {
// 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,
description: component.description || '',
dependencies: deps,
code: component.code,
isActive: component.isActive,
}
setInitialValues(values)
parseAndUpdateComponents(component.code)
setIsLoaded(true)
}
} else if (!isEditing && !isLoaded) {
// Yeni komponent için boş başla - TEMPLATE YOK
setIsLoaded(true)
}
}, [id, isEditing, getComponent, isLoaded, parseAndUpdateComponents])
// Generate component template based on name
const generateComponentTemplate = (componentName: string): string => {
if (!componentName.trim()) return ''
// Convert component name to PascalCase if not already
const pascalCaseName = componentName.charAt(0).toUpperCase() + componentName.slice(1)
return `const ${pascalCaseName}Component = ({
title = "${pascalCaseName}"
}) => {
return (
<span>{title}</span>
);
};
export default ${pascalCaseName}Component;`
}
const handleSubmit = async (values: typeof initialValues, { setSubmitting }: any) => {
if (!values.name.trim()) {
alert('Please enter a component name')
setSubmitting(false)
return
}
const criticalErrors = validationErrors.filter((error) => {
const message = error.message.toLowerCase()
return (
!message.includes('can only be used in typescript files') &&
!message.includes('type annotations can only be used') &&
!message.includes('jsx-runtime') &&
!message.includes('corresponding type declarations') &&
error.severity === 8
)
})
if (criticalErrors.length > 0) {
const proceed = window.confirm(
`There are ${criticalErrors.length} critical error(s) in your code. Do you want to save anyway?`,
)
if (!proceed) {
setSubmitting(false)
return
}
}
try {
const componentData = {
name: values.name.trim(),
description: values.description.trim(),
dependencies: JSON.stringify(values.dependencies), // Serialize dependencies to JSON string
code: values.code.trim(),
isActive: values.isActive,
}
if (isEditing && id) {
updateComponent(id, componentData)
} else {
addComponent(componentData)
}
navigate(ROUTES_ENUM.protected.saas.developerKit.components)
} 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="h-screen flex 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.Loading')}</p>
</div>
</div>
)
}
return (
<Formik
enableReinitialize
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => {
return (
<>
{/* Enhanced Header */}
<div className="bg-white dark:bg-gray-900 shadow-lg border-b border-slate-200 dark:border-gray-700 sticky top-0 z-10">
<div className="px-1 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center gap-3 pl-3">
<div className="bg-gradient-to-r from-blue-500 to-purple-600 p-2 rounded-lg">
<FaCode className="w-5 h-5 text-white" />
</div>
<div>
<h1 className="font-semibold text-slate-800 dark:text-gray-100 text-sm leading-tight">
{isEditing
? `${translate('::App.DeveloperKit.ComponentEditor.Title.Edit')} - ${values.name || initialValues.name || 'Component'}`
: translate('::App.DeveloperKit.ComponentEditor.Title.Create')}
</h1>
<p className="text-xs text-slate-500 dark:text-gray-400 leading-tight">
{isEditing ? 'Modify your React component' : 'Create a new React component'}
</p>
</div>
</div>
</div>
{/* Save Button in Header */}
<div className="flex items-center gap-3 pr-3">
<Link
to={ROUTES_ENUM.protected.saas.developerKit.components}
className="flex items-center gap-2 text-slate-600 dark:text-gray-300 text-black dark:text-white px-4 py-2 rounded-lg hover:text-slate-700 dark:hover:text-gray-100 transition-colors"
>
<FaArrowLeft className="w-3.5 h-3.5" />
{translate('::App.DeveloperKit.ComponentEditor.Back')}
</Link>
<div className="h-6 w-px bg-slate-300 dark:bg-gray-700"></div>
<Button
type="button"
variant='solid'
size="sm"
color='green'
onClick={submitForm}
disabled={isSubmitting || !values.name.trim() || !isValid}
className="flex items-center gap-2 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 dark:hover:bg-emerald-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<FaRegSave className="w-4 h-4" />
{isSubmitting
? translate('::App.DeveloperKit.ComponentEditor.Saving')
: translate('::App.DeveloperKit.ComponentEditor.Save')}
</Button>
</div>
</div>
</div>
</div>
<Form className="grid grid-cols-1 lg:grid-cols-3 gap-4 py-3">
<div className="space-y-3 col-span-1">
<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.DeveloperKit.ComponentEditor.ComponentName')}
invalid={!!(errors.name && touched.name)}
errorMessage={errors.name as string}
>
<Field
name="name"
type="text"
component={Input}
autoFocus
placeholder="e.g., Button, Card, Modal"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const newName = e.target.value
setFieldValue('name', newName)
// Auto-generate code template if this is a new component (not editing)
// and only if the code field is empty or contains the default template
if (!isEditing && newName.trim()) {
const currentCode = values.code.trim()
const isCodeEmpty = !currentCode
const isCodeDefaultTemplate = currentCode.includes('Component = ({') &&
currentCode.includes('export default') &&
currentCode.includes('<span>{title}</span>')
if (isCodeEmpty || isCodeDefaultTemplate) {
const template = generateComponentTemplate(newName)
setFieldValue('code', template)
parseAndUpdateComponents(template)
}
}
} } />
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsDescription')}
invalid={!!(errors.description && touched.description)}
errorMessage={errors.description as string}
>
<Field
name="description"
type="text"
component={Input}
placeholder="Brief description of the component" />
</FormItem>
<FormItem
label={translate('::App.Platform.Code')}
invalid={!!(errors.code && touched.code)}
errorMessage={errors.code as string}
>
<Field
name="code"
type="text"
component={Input}
placeholder="React component code goes here"
textArea={true}
rows={10}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
setFieldValue('code', e.target.value)
} } />
</FormItem>
<FormItem
label={translate('::App.DeveloperKit.ComponentEditor.Dependencies')}
invalid={!!(errors.dependencies && touched.dependencies)}
errorMessage={errors.dependencies as string}
>
<Field name="dependencies">
{({ field }: FieldProps) => (
<Input
type="text"
value={(values.dependencies || []).join(', ')}
onChange={(e) => setFieldValue(
'dependencies',
e.target.value
.split(',')
.map((s) => s.trim())
.filter(Boolean)
)}
placeholder="MyComponent, AnotherComponent, etc." />
)}
</Field>
</FormItem>
<FormItem label={translate('::App.Status.Active')}>
<Field name="isActive" component={Checkbox} />
</FormItem>
</FormContainer>
</div>
</div>
{/* Right Side - Preview and Validation */}
<div className="space-y-4 col-span-2">
{/* Validation Errors */}
{validationErrors.length > 0 && (
<div className="bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-700 rounded-lg p-3 shadow-sm">
<div className="flex items-start gap-2">
<div className="bg-red-100 dark:bg-red-900 rounded-full p-1.5">
<FaExclamationCircle className="w-4 h-4 text-red-600 dark:text-red-400" />
</div>
<div className="flex-1">
<h3 className="text-base font-semibold text-red-800 dark:text-red-300 mb-1">
Validation Issues
</h3>
<p className="text-xs text-red-700 dark:text-red-400 mb-3">
{validationErrors.length} issue
{validationErrors.length !== 1 ? 's' : ''} found in your code
</p>
<div className="space-y-1.5 max-h-32 overflow-y-auto">
{validationErrors.slice(0, 5).map((error, index) => (
<div
key={index}
className="bg-white dark:bg-gray-900 p-2 rounded border border-red-100 dark:border-red-700"
>
<div className="text-xs text-red-800 dark:text-red-300">
<span className="font-medium bg-red-100 dark:bg-red-900 px-1.5 py-0.5 rounded text-xs">
Line {error.startLineNumber}
</span>
<span className="ml-2">{error.message}</span>
</div>
</div>
))}
{validationErrors.length > 5 && (
<div className="text-xs text-red-600 dark:text-red-400 italic text-center py-1">
... and {validationErrors.length - 5} more issue
{validationErrors.length - 5 !== 1 ? 's' : ''}
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* Component Preview */}
<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-purple-100 dark:bg-purple-900/20 p-1.5 rounded-lg">
<FaEye className="w-4 h-4 text-purple-600 dark:text-purple-400" />
</div>
<h2 className="text-base font-semibold text-slate-900 dark:text-gray-100">
Preview
</h2>
</div>
<ComponentPreview componentName={values.name} />
</div>
</div>
</Form>
</>
)
}}
</Formik>
)
}
export default ComponentEditor