mirror of
https://github.com/outbackdingo/openapi-ui.git
synced 2026-01-27 18:19:50 +00:00
Merge pull request #6 from PRO-Robotech/feature/Factory-Admin
feature/Factory Admin
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -11,7 +11,7 @@
|
||||
"@ant-design/icons": "5.6.0",
|
||||
"@monaco-editor/react": "4.6.0",
|
||||
"@originjs/vite-plugin-federation": "1.3.6",
|
||||
"@prorobotech/openapi-k8s-toolkit": "^0.0.1-alpha.20",
|
||||
"@prorobotech/openapi-k8s-toolkit": "^0.0.1-alpha.21",
|
||||
"@readme/openapi-parser": "4.0.0",
|
||||
"@reduxjs/toolkit": "2.2.5",
|
||||
"@tanstack/react-query": "5.62.2",
|
||||
@@ -2752,9 +2752,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prorobotech/openapi-k8s-toolkit": {
|
||||
"version": "0.0.1-alpha.20",
|
||||
"resolved": "https://registry.npmjs.org/@prorobotech/openapi-k8s-toolkit/-/openapi-k8s-toolkit-0.0.1-alpha.20.tgz",
|
||||
"integrity": "sha512-sooiZUyFwOpc0iUX+uNGy9Qr1iwJkjvn286dkCAyuWbLjhWPXUMi5hiJ1Y0DdtdeZV31zHIx+bVFJ1jBhw1YsQ==",
|
||||
"version": "0.0.1-alpha.21",
|
||||
"resolved": "https://registry.npmjs.org/@prorobotech/openapi-k8s-toolkit/-/openapi-k8s-toolkit-0.0.1-alpha.21.tgz",
|
||||
"integrity": "sha512-1uXnvf5s82VyuJTnNwmliQtIOtIMmd20/xwXHN4uh0BP8Y6dOCpUP5tGUlo7zhbvm6Gh2uZhYhSFqqxZPw11Kw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "4.6.0",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"@ant-design/icons": "5.6.0",
|
||||
"@monaco-editor/react": "4.6.0",
|
||||
"@originjs/vite-plugin-federation": "1.3.6",
|
||||
"@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.20",
|
||||
"@prorobotech/openapi-k8s-toolkit": "0.0.1-alpha.21",
|
||||
"@readme/openapi-parser": "4.0.0",
|
||||
"@reduxjs/toolkit": "2.2.5",
|
||||
"@tanstack/react-query": "5.62.2",
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
FormApiPage,
|
||||
FormCrdPage,
|
||||
FactoryPage,
|
||||
FactoryAdminPage,
|
||||
} from 'pages'
|
||||
import { getBasePrefix } from 'utils/getBaseprefix'
|
||||
import { colorsLight, colorsDark, sizes } from 'constants/colors'
|
||||
@@ -127,6 +128,7 @@ export const App: FC<TAppProps> = ({ isFederation, forcedTheme }) => {
|
||||
element={<FormCrdPage forcedTheme={forcedTheme} />}
|
||||
/>
|
||||
<Route path={`${prefix}/:clusterName/factory/:key/*`} element={<FactoryPage forcedTheme={forcedTheme} />} />
|
||||
<Route path={`${prefix}/factoryAdmin/*`} element={<FactoryAdminPage />} />
|
||||
</Routes>
|
||||
)
|
||||
|
||||
|
||||
52
src/pages/FactoryAdminPage/AddComponentModal.tsx
Normal file
52
src/pages/FactoryAdminPage/AddComponentModal.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Modal, Button, Form, Select } from 'antd'
|
||||
import { ComponentType } from './types'
|
||||
|
||||
export const componentTypes: ComponentType[] = [
|
||||
'antdText',
|
||||
'antdCard',
|
||||
'antdFlex',
|
||||
'antdRow',
|
||||
'antdCol',
|
||||
'partsOfUrl',
|
||||
'multiQuery',
|
||||
'parsedText',
|
||||
]
|
||||
|
||||
interface AddComponentModalProps {
|
||||
onAdd: (type: ComponentType) => void
|
||||
title?: string
|
||||
}
|
||||
|
||||
export const AddComponentModal: React.FC<AddComponentModalProps> = ({ onAdd, title = 'Add Component' }) => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const handleSubmit = (values: { type: ComponentType }) => {
|
||||
onAdd(values.type)
|
||||
setVisible(false)
|
||||
form.resetFields()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setVisible(true)}>Add</Button>
|
||||
<Modal title={title} visible={visible} onCancel={() => setVisible(false)} footer={null}>
|
||||
<Form form={form} onFinish={handleSubmit}>
|
||||
<Form.Item label="Component Type" name="type" rules={[{ required: true }]}>
|
||||
<Select>
|
||||
{componentTypes.map(type => (
|
||||
<Select.Option key={type} value={type}>
|
||||
{type}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Add
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
73
src/pages/FactoryAdminPage/AntdCardForm.tsx
Normal file
73
src/pages/FactoryAdminPage/AntdCardForm.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Input, Select, Button, Switch } from 'antd'
|
||||
|
||||
const { TextArea } = Input
|
||||
const { Option } = Select
|
||||
|
||||
interface AntdCardFormProps {
|
||||
initialValues: any // This would be `TDynamicComponentsAppTypeMap['antdCard']`
|
||||
onSave: (data: any) => void
|
||||
}
|
||||
|
||||
export const AntdCardForm: React.FC<AntdCardFormProps> = ({ initialValues, onSave }) => {
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(initialValues)
|
||||
}, [initialValues, form])
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
try {
|
||||
if (values.style) {
|
||||
values.style = JSON.parse(values.style)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Invalid JSON for style', e)
|
||||
return
|
||||
}
|
||||
|
||||
onSave(values)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form form={form} onFinish={handleSubmit} layout="vertical" initialValues={initialValues}>
|
||||
<Form.Item label="ID" name="id" rules={[{ required: true }]}>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Title" name="title">
|
||||
<Input placeholder="Card title" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Bordered" name="bordered" valuePropName="checked">
|
||||
<Switch defaultChecked />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Cover Image URL" name="cover">
|
||||
<Input placeholder="https://example.com/image.jpg" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Style (JSON)" name="style">
|
||||
<TextArea rows={4} placeholder='e.g. {"width": "300px", "margin": "10px"}' />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Size" name="size">
|
||||
<Select defaultValue="default">
|
||||
<Option value="default">Default</Option>
|
||||
<Option value="small">Small</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Extra Content (Right Side)" name="extra">
|
||||
<Input placeholder="Extra content like links or buttons" />
|
||||
</Form.Item>
|
||||
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
55
src/pages/FactoryAdminPage/AntdTextForm.tsx
Normal file
55
src/pages/FactoryAdminPage/AntdTextForm.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Input, Select, Button } from 'antd'
|
||||
|
||||
interface AntdTextFormProps {
|
||||
initialValues: any
|
||||
onSave: (data: any) => void
|
||||
}
|
||||
|
||||
export const AntdTextForm: React.FC<AntdTextFormProps> = ({ initialValues, onSave }) => {
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(initialValues)
|
||||
}, [initialValues, form])
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
try {
|
||||
if (values.style) {
|
||||
values.style = JSON.parse(values.style)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Invalid JSON for style', e)
|
||||
}
|
||||
onSave(values)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form form={form} onFinish={handleSubmit} layout="vertical" initialValues={initialValues}>
|
||||
<Form.Item label="ID" name="id" rules={[{ required: true }]}>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item label="Text" name="text" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Type" name="type">
|
||||
<Select allowClear>
|
||||
<Select.Option value="">Default</Select.Option>
|
||||
<Select.Option value="secondary">Secondary</Select.Option>
|
||||
<Select.Option value="success">Success</Select.Option>
|
||||
<Select.Option value="warning">Warning</Select.Option>
|
||||
<Select.Option value="danger">Danger</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="Style (JSON)" name="style">
|
||||
<Input.TextArea rows={4} placeholder='e.g. {"color": "red"}' />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
127
src/pages/FactoryAdminPage/AppComponentAdmin.tsx
Normal file
127
src/pages/FactoryAdminPage/AppComponentAdmin.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
/* eslint-disable no-console */
|
||||
import React, { useState } from 'react'
|
||||
import { Button, Typography, Modal, Input } from 'antd'
|
||||
|
||||
import { ComponentNode } from './ComponentNode'
|
||||
import { AddComponentModal } from './AddComponentModal'
|
||||
import { Component, ComponentType, TDynamicComponentsAppTypeMap } from './types'
|
||||
|
||||
const { Title } = Typography
|
||||
const { TextArea } = Input
|
||||
|
||||
export const AppComponentAdmin: React.FC = () => {
|
||||
const [components, setComponents] = useState<Component[]>([])
|
||||
const [maxId, setMaxId] = useState(1)
|
||||
const [jsonOutput, setJsonOutput] = useState<string>('')
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
|
||||
const getNewId = () => {
|
||||
const newId = maxId + 1
|
||||
setMaxId(newId)
|
||||
return newId
|
||||
}
|
||||
|
||||
const generateDefaultData = (type: ComponentType): TDynamicComponentsAppTypeMap[ComponentType] => {
|
||||
const base = { id: getNewId() }
|
||||
switch (type) {
|
||||
case 'antdText':
|
||||
return { ...base, text: '' }
|
||||
case 'antdCard':
|
||||
return { ...base, title: '' }
|
||||
case 'antdFlex':
|
||||
return { ...base, justify: 'start', align: 'start' }
|
||||
case 'antdRow':
|
||||
return { ...base }
|
||||
case 'antdCol':
|
||||
return { ...base, span: 12 }
|
||||
case 'partsOfUrl':
|
||||
case 'multiQuery':
|
||||
case 'parsedText':
|
||||
return { ...base, text: '' }
|
||||
default:
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
const addComponent = (type: ComponentType) => {
|
||||
const newComponent: Component = {
|
||||
type,
|
||||
data: generateDefaultData(type),
|
||||
children: [],
|
||||
}
|
||||
setComponents(prev => [...prev, newComponent])
|
||||
}
|
||||
|
||||
const updateComponent = (index: number, updated: Component) => {
|
||||
setComponents(prev => {
|
||||
const newComponents = [...prev]
|
||||
newComponents[index] = updated
|
||||
return newComponents
|
||||
})
|
||||
}
|
||||
|
||||
const deleteComponent = (index: number) => {
|
||||
setComponents(prev => {
|
||||
const newComponents = [...prev]
|
||||
newComponents.splice(index, 1)
|
||||
return newComponents
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveAll = () => {
|
||||
const output = JSON.stringify(
|
||||
{
|
||||
data: components,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)
|
||||
setJsonOutput(output)
|
||||
setIsModalVisible(true)
|
||||
}
|
||||
|
||||
const handleCopyToClipboard = () => {
|
||||
navigator.clipboard.writeText(jsonOutput)
|
||||
console.log('Copied to clipboard!')
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<Title level={3}>Component Admin Panel</Title>
|
||||
<AddComponentModal onAdd={addComponent} title="Add Root Component" />
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
{components.map((component, index) => (
|
||||
<ComponentNode
|
||||
key={component.data.id}
|
||||
component={component}
|
||||
onUpdate={updated => updateComponent(index, updated)}
|
||||
onDelete={() => deleteComponent(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<Button type="primary" onClick={handleSaveAll} style={{ marginTop: 20 }}>
|
||||
Save All as JSON
|
||||
</Button>
|
||||
|
||||
{/* JSON Output Modal */}
|
||||
<Modal
|
||||
title="Final JSON Output"
|
||||
open={isModalVisible}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="copy" onClick={handleCopyToClipboard}>
|
||||
Copy to Clipboard
|
||||
</Button>,
|
||||
<Button key="close" onClick={() => setIsModalVisible(false)}>
|
||||
Close
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<TextArea value={jsonOutput} readOnly rows={20} style={{ fontFamily: 'monospace' }} />
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
15
src/pages/FactoryAdminPage/ComponentForm.tsx
Normal file
15
src/pages/FactoryAdminPage/ComponentForm.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// src/ComponentForm.tsx
|
||||
import React from 'react'
|
||||
import { DynamicComponentForm } from './DynamicComponentForm'
|
||||
import type { ComponentType } from './types'
|
||||
|
||||
interface ComponentFormProps {
|
||||
type: ComponentType
|
||||
data: any
|
||||
onSave: (data: any) => void
|
||||
}
|
||||
|
||||
export const ComponentForm: React.FC<ComponentFormProps> = ({ type, data, onSave }) => {
|
||||
return <DynamicComponentForm type={type} initialValues={data} onSave={onSave} />
|
||||
}
|
||||
27
src/pages/FactoryAdminPage/ComponentForm2.tsx
Normal file
27
src/pages/FactoryAdminPage/ComponentForm2.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React from 'react'
|
||||
import { ComponentType } from './types'
|
||||
import { AntdTextForm } from './AntdTextForm'
|
||||
import { AntdCardForm } from './AntdCardForm'
|
||||
import { TextWithIdForm } from './TextWithIdForm'
|
||||
|
||||
interface ComponentFormProps {
|
||||
type: ComponentType
|
||||
data: any
|
||||
onSave: (data: any) => void
|
||||
}
|
||||
|
||||
export const ComponentForm: React.FC<ComponentFormProps> = ({ type, data, onSave }) => {
|
||||
switch (type) {
|
||||
case 'antdText':
|
||||
return <AntdTextForm initialValues={data} onSave={onSave} />
|
||||
case 'antdCard':
|
||||
return <AntdCardForm initialValues={data} onSave={onSave} />
|
||||
case 'partsOfUrl':
|
||||
case 'multiQuery':
|
||||
case 'parsedText':
|
||||
return <TextWithIdForm initialValues={data} onSave={onSave} />
|
||||
default:
|
||||
return <div>Unsupported component type</div>
|
||||
}
|
||||
}
|
||||
64
src/pages/FactoryAdminPage/ComponentNode.tsx
Normal file
64
src/pages/FactoryAdminPage/ComponentNode.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react'
|
||||
import { Button } from 'antd'
|
||||
import { Component, ComponentType } from './types'
|
||||
import { EditComponentModal } from './EditComponentModal'
|
||||
import { AddComponentModal } from './AddComponentModal'
|
||||
import { generateDefaultData } from './utils'
|
||||
|
||||
interface ComponentNodeProps {
|
||||
component: Component
|
||||
onUpdate: (component: Component) => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
export const ComponentNode: React.FC<ComponentNodeProps> = ({ component, onUpdate, onDelete }) => {
|
||||
const handleAddChild = (type: ComponentType) => {
|
||||
const newChild: Component = {
|
||||
type,
|
||||
data: generateDefaultData(type),
|
||||
children: [],
|
||||
}
|
||||
const updated = { ...component }
|
||||
updated.children = updated.children ? [...updated.children, newChild] : [newChild]
|
||||
onUpdate(updated)
|
||||
}
|
||||
|
||||
const handleUpdateChild = (index: number, updatedChild: Component) => {
|
||||
const updated = { ...component }
|
||||
updated.children![index] = updatedChild
|
||||
onUpdate(updated)
|
||||
}
|
||||
|
||||
const handleDeleteChild = (index: number) => {
|
||||
const updated = { ...component }
|
||||
updated.children!.splice(index, 1)
|
||||
onUpdate(updated)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ border: '1px solid #ccc', padding: 10, margin: '10px 0' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>
|
||||
<strong>{component.type}</strong> (ID: {component.data.id})
|
||||
</span>
|
||||
<div>
|
||||
<EditComponentModal component={component} onSave={onUpdate} />
|
||||
<AddComponentModal onAdd={handleAddChild} title="Add Child" />
|
||||
<Button danger size="small" onClick={onDelete} style={{ marginLeft: 8 }}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginLeft: 20, marginTop: 10 }}>
|
||||
{component.children?.map((child, idx) => (
|
||||
<ComponentNode
|
||||
key={child.data.id}
|
||||
component={child}
|
||||
onUpdate={updated => handleUpdateChild(idx, updated)}
|
||||
onDelete={() => handleDeleteChild(idx)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
111
src/pages/FactoryAdminPage/DynamicComponentForm.tsx
Normal file
111
src/pages/FactoryAdminPage/DynamicComponentForm.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// src/DynamicComponentForm.tsx
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Input, Select, Switch, Button, Typography } from 'antd'
|
||||
|
||||
import { componentMetaMap } from './utils'
|
||||
import type { ComponentType } from './types'
|
||||
|
||||
const { TextArea } = Input
|
||||
const { Option } = Select
|
||||
|
||||
interface DynamicComponentFormProps {
|
||||
type: ComponentType
|
||||
initialValues: any
|
||||
onSave: (data: any) => void
|
||||
}
|
||||
|
||||
export const DynamicComponentForm: React.FC<DynamicComponentFormProps> = ({ type, initialValues, onSave }) => {
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(initialValues)
|
||||
}, [initialValues, form])
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
try {
|
||||
const parsedValues = Object.entries(values).reduce((acc, [key, value]) => {
|
||||
const meta = componentMetaMap[type]?.[key]
|
||||
|
||||
if (meta?.inputType === 'json') {
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
acc[key] = JSON.parse(value)
|
||||
} catch (e) {
|
||||
console.warn(`Invalid JSON for ${key}:`, value)
|
||||
acc[key] = value // Keep original string
|
||||
}
|
||||
} else {
|
||||
acc[key] = value // Already parsed or non-string
|
||||
}
|
||||
} else {
|
||||
acc[key] = value
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {} as any)
|
||||
|
||||
onSave(parsedValues)
|
||||
} catch (e) {
|
||||
console.log('Invalid JSON input', e)
|
||||
}
|
||||
}
|
||||
|
||||
const renderFormItem = (prop: string, meta: any) => {
|
||||
const value = initialValues[prop]
|
||||
|
||||
switch (meta.inputType) {
|
||||
case 'text':
|
||||
return <Input placeholder={meta.label} />
|
||||
case 'textarea':
|
||||
return <TextArea rows={4} placeholder={meta.label} />
|
||||
case 'json':
|
||||
return (
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder={`e.g. {"color": "red"}`}
|
||||
defaultValue={value ? JSON.stringify(value, null, 2) : ''}
|
||||
/>
|
||||
)
|
||||
case 'boolean':
|
||||
return <Switch defaultChecked={value} />
|
||||
case 'select':
|
||||
return (
|
||||
<Select defaultValue={value}>
|
||||
{meta.options?.map((opt: { label: string; value: string }) => (
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
case 'number':
|
||||
return <Input type="number" placeholder={meta.label} />
|
||||
default:
|
||||
return <Input />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form form={form} onFinish={handleSubmit} layout="vertical">
|
||||
{/* ID (always present and read-only) */}
|
||||
<Form.Item label="ID" name="id" rules={[{ required: true }]}>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
|
||||
{/* Render dynamic props */}
|
||||
{Object.entries(componentMetaMap[type] || {}).map(([prop, meta]) => (
|
||||
<Form.Item key={prop} label={meta.label} name={prop}>
|
||||
{renderFormItem(prop, meta)}
|
||||
</Form.Item>
|
||||
))}
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button type="primary" htmlType="submit" style={{ marginTop: 16 }}>
|
||||
Save
|
||||
</Button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
34
src/pages/FactoryAdminPage/EditComponentModal.tsx
Normal file
34
src/pages/FactoryAdminPage/EditComponentModal.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable react/button-has-type */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React, { useState } from 'react'
|
||||
import { Modal } from 'antd'
|
||||
import { ComponentForm } from './ComponentForm'
|
||||
import { Component } from './types'
|
||||
|
||||
interface EditComponentModalProps {
|
||||
component: Component
|
||||
onSave: (component: Component) => void
|
||||
}
|
||||
|
||||
export const EditComponentModal: React.FC<EditComponentModalProps> = ({ component, onSave }) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleSave = (data: any) => {
|
||||
onSave({ ...component, data })
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => setOpen(true)}>Edit</button>
|
||||
<Modal
|
||||
title={`Edit ${component.type}`}
|
||||
open={open} // ✅ Use open instead of visible
|
||||
onCancel={() => setOpen(false)}
|
||||
footer={null}
|
||||
>
|
||||
<ComponentForm type={component.type} data={component.data} onSave={handleSave} />
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
39
src/pages/FactoryAdminPage/TextWithIdForm.tsx
Normal file
39
src/pages/FactoryAdminPage/TextWithIdForm.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { Form, Input, Button } from 'antd'
|
||||
|
||||
interface TextWithIdFormProps {
|
||||
initialValues: {
|
||||
id: number
|
||||
text: string
|
||||
}
|
||||
onSave: (data: { id: number; text: string }) => void
|
||||
}
|
||||
|
||||
export const TextWithIdForm: React.FC<TextWithIdFormProps> = ({ initialValues, onSave }) => {
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(initialValues)
|
||||
}, [initialValues, form])
|
||||
|
||||
const handleSubmit = (values: { id: number; text: string }) => {
|
||||
onSave(values)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form form={form} onFinish={handleSubmit} layout="vertical" initialValues={initialValues}>
|
||||
<Form.Item label="ID" name="id" rules={[{ required: true }]}>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item label="Text" name="text" rules={[{ required: true }]}>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
placeholder="Enter text (you can use variables like {5}, {reqs[0]['metadata']} etc.)"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
1
src/pages/FactoryAdminPage/index.ts
Normal file
1
src/pages/FactoryAdminPage/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { AppComponentAdmin as FactoryAdminPage } from './AppComponentAdmin'
|
||||
26
src/pages/FactoryAdminPage/types.ts
Normal file
26
src/pages/FactoryAdminPage/types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type {
|
||||
CardProps as AntdCardProps,
|
||||
FlexProps as AntdFlexProps,
|
||||
RowProps as AntdRowProps,
|
||||
ColProps as AntdColProps,
|
||||
} from 'antd'
|
||||
import { TextProps as AntdTextProps } from 'antd/es/typography/Text'
|
||||
|
||||
export type TDynamicComponentsAppTypeMap = {
|
||||
antdText: { id: number; text: string } & Omit<AntdTextProps, 'id'>
|
||||
antdCard: { id: number } & Omit<AntdCardProps, 'id'>
|
||||
antdFlex: { id: number } & Omit<AntdFlexProps, 'id' | 'children'>
|
||||
antdRow: { id: number } & Omit<AntdRowProps, 'id' | 'children'>
|
||||
antdCol: { id: number } & Omit<AntdColProps, 'id' | 'children'>
|
||||
partsOfUrl: { id: number; text: string }
|
||||
multiQuery: { id: number; text: string }
|
||||
parsedText: { id: number; text: string }
|
||||
}
|
||||
|
||||
export type ComponentType = keyof TDynamicComponentsAppTypeMap
|
||||
|
||||
export type Component = {
|
||||
type: ComponentType
|
||||
data: TDynamicComponentsAppTypeMap[ComponentType]
|
||||
children?: Component[]
|
||||
}
|
||||
111
src/pages/FactoryAdminPage/utils.ts
Normal file
111
src/pages/FactoryAdminPage/utils.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// src/utils.ts
|
||||
import type { ComponentType, TDynamicComponentsAppTypeMap } from './types'
|
||||
|
||||
export const generateDefaultData = (type: ComponentType): TDynamicComponentsAppTypeMap[ComponentType] => {
|
||||
const base = { id: Math.floor(Math.random() * 100000) } // Simplified ID generation
|
||||
switch (type) {
|
||||
case 'antdText':
|
||||
return { ...base, text: '' }
|
||||
case 'antdCard':
|
||||
return { ...base, title: '' }
|
||||
case 'antdFlex':
|
||||
return { ...base, justify: 'start', align: 'start' }
|
||||
case 'antdRow':
|
||||
return { ...base }
|
||||
case 'antdCol':
|
||||
return { ...base, span: 12 }
|
||||
case 'partsOfUrl':
|
||||
case 'multiQuery':
|
||||
case 'parsedText':
|
||||
return { ...base, text: '' }
|
||||
default:
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
// Define property metadata for each component
|
||||
type PropertyMeta = {
|
||||
label: string
|
||||
inputType: 'text' | 'textarea' | 'json' | 'boolean' | 'select' | 'number'
|
||||
options?: { label: string; value: any }[]
|
||||
}
|
||||
|
||||
type ComponentMetaMap = {
|
||||
[key in ComponentType]: Record<string, PropertyMeta>
|
||||
}
|
||||
|
||||
export const componentMetaMap: ComponentMetaMap = {
|
||||
antdText: {
|
||||
text: { label: 'Text', inputType: 'textarea' },
|
||||
type: {
|
||||
label: 'Type',
|
||||
inputType: 'select',
|
||||
options: [
|
||||
{ label: 'Default', value: '' },
|
||||
{ label: 'Secondary', value: 'secondary' },
|
||||
{ label: 'Success', value: 'success' },
|
||||
{ label: 'Warning', value: 'warning' },
|
||||
{ label: 'Danger', value: 'danger' },
|
||||
],
|
||||
},
|
||||
style: { label: 'Style (JSON)', inputType: 'json' },
|
||||
},
|
||||
antdCard: {
|
||||
title: { label: 'Title', inputType: 'text' },
|
||||
bordered: { label: 'Bordered', inputType: 'boolean' },
|
||||
cover: { label: 'Cover Image URL', inputType: 'text' },
|
||||
size: {
|
||||
label: 'Size',
|
||||
inputType: 'select',
|
||||
options: [
|
||||
{ label: 'Default', value: 'default' },
|
||||
{ label: 'Small', value: 'small' },
|
||||
],
|
||||
},
|
||||
extra: { label: 'Extra Content', inputType: 'text' },
|
||||
style: { label: 'Style (JSON)', inputType: 'json' },
|
||||
},
|
||||
antdFlex: {
|
||||
justify: {
|
||||
label: 'Justify',
|
||||
inputType: 'select',
|
||||
options: [
|
||||
{ label: 'Start', value: 'start' },
|
||||
{ label: 'End', value: 'end' },
|
||||
{ label: 'Center', value: 'center' },
|
||||
{ label: 'Space Around', value: 'space-around' },
|
||||
{ label: 'Space Between', value: 'space-between' },
|
||||
],
|
||||
},
|
||||
align: {
|
||||
label: 'Align',
|
||||
inputType: 'select',
|
||||
options: [
|
||||
{ label: 'Start', value: 'start' },
|
||||
{ label: 'End', value: 'end' },
|
||||
{ label: 'Center', value: 'center' },
|
||||
{ label: 'Baseline', value: 'baseline' },
|
||||
{ label: 'Stretch', value: 'stretch' },
|
||||
],
|
||||
},
|
||||
gap: { label: 'Gap (number)', inputType: 'number' },
|
||||
},
|
||||
antdRow: {
|
||||
gutter: { label: 'Gutter', inputType: 'number' },
|
||||
style: { label: 'Style (JSON)', inputType: 'json' },
|
||||
},
|
||||
antdCol: {
|
||||
span: { label: 'Span (1-24)', inputType: 'number' },
|
||||
offset: { label: 'Offset', inputType: 'number' },
|
||||
},
|
||||
partsOfUrl: {
|
||||
text: { label: 'Text', inputType: 'textarea' },
|
||||
},
|
||||
multiQuery: {
|
||||
text: { label: 'Text', inputType: 'textarea' },
|
||||
},
|
||||
parsedText: {
|
||||
text: { label: 'Text', inputType: 'textarea' },
|
||||
},
|
||||
}
|
||||
@@ -16,3 +16,4 @@ export { FormCrdPage } from './FormCrdPage'
|
||||
export { FormApiPage } from './FormApiPage'
|
||||
export { FormBuiltinPage } from './FormBuiltinPage'
|
||||
export { FactoryPage } from './FactoryPage'
|
||||
export { FactoryAdminPage } from './FactoryAdminPage'
|
||||
|
||||
Reference in New Issue
Block a user