mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
feat: migrate DataSourceManager to JSON (Tier 2 - Organism 1)
- Create JSON definition in src/components/json-definitions/data-source-manager.json - Create TypeScript interface in src/lib/json-ui/interfaces/data-source-manager.ts - Create custom hook useDataSourceManagerState in src/hooks/use-data-source-manager-state.ts - Register hook in src/lib/json-ui/hooks-registry.ts - Export DataSourceManager from src/lib/json-ui/json-components.ts - Update imports in src/components/DataBindingDesigner.tsx - Remove legacy TSX files and sub-components - Update exports in src/components/organisms/index.ts - Add hook export in src/hooks/index.ts DataSourceManager now uses JSON-driven architecture with custom hooks for state management. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { DataSourceManager } from '@/components/organisms/DataSourceManager'
|
||||
import { ComponentBindingDialog } from '@/lib/json-ui/json-components'
|
||||
import { DataSourceManager, ComponentBindingDialog } from '@/lib/json-ui/json-components'
|
||||
import { DataSource, UIComponent } from '@/types/json-ui'
|
||||
import { DataBindingHeader } from '@/components/data-binding-designer/DataBindingHeader'
|
||||
import { ComponentBindingsCard } from '@/components/data-binding-designer/ComponentBindingsCard'
|
||||
|
||||
265
src/components/json-definitions/data-source-manager.json
Normal file
265
src/components/json-definitions/data-source-manager.json
Normal file
@@ -0,0 +1,265 @@
|
||||
{
|
||||
"id": "data-source-manager",
|
||||
"type": "div",
|
||||
"props": { "className": "space-y-6" },
|
||||
"children": [
|
||||
{
|
||||
"id": "data-sources-card",
|
||||
"type": "Card",
|
||||
"children": [
|
||||
{
|
||||
"id": "card-header",
|
||||
"type": "CardHeader",
|
||||
"children": [
|
||||
{
|
||||
"id": "header-container",
|
||||
"type": "div",
|
||||
"props": { "className": "flex items-center justify-between" },
|
||||
"children": [
|
||||
{
|
||||
"id": "header-content",
|
||||
"type": "Stack",
|
||||
"props": { "direction": "vertical", "spacing": "xs" },
|
||||
"children": [
|
||||
{
|
||||
"type": "Heading",
|
||||
"bindings": { "children": "headerCopy.title" },
|
||||
"props": { "level": 2 }
|
||||
},
|
||||
{
|
||||
"type": "Text",
|
||||
"bindings": { "children": "headerCopy.description" },
|
||||
"props": { "variant": "muted" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "add-button-group",
|
||||
"type": "div",
|
||||
"children": [
|
||||
{
|
||||
"type": "DropdownMenu",
|
||||
"children": [
|
||||
{
|
||||
"type": "DropdownMenuTrigger",
|
||||
"props": { "asChild": true },
|
||||
"children": [
|
||||
{
|
||||
"type": "div",
|
||||
"children": [
|
||||
{
|
||||
"type": "ActionButton",
|
||||
"bindings": {
|
||||
"label": "headerCopy.addLabel",
|
||||
"onClick": {
|
||||
"source": "",
|
||||
"transform": "() => {}"
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"variant": "default"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "PhosphorIcon",
|
||||
"props": { "icon": "Plus", "className": "w-4 h-4" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "DropdownMenuContent",
|
||||
"props": { "align": "end" },
|
||||
"children": [
|
||||
{
|
||||
"type": "DropdownMenuItem",
|
||||
"bindings": {
|
||||
"onClick": {
|
||||
"source": "addDataSource",
|
||||
"transform": "() => addDataSource('kv')"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "PhosphorIcon",
|
||||
"props": { "icon": "Database", "className": "w-4 h-4 mr-2" }
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"bindings": { "children": "headerCopy.menu.kv" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "DropdownMenuItem",
|
||||
"bindings": {
|
||||
"onClick": {
|
||||
"source": "addDataSource",
|
||||
"transform": "() => addDataSource('static')"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "PhosphorIcon",
|
||||
"props": { "icon": "FileText", "className": "w-4 h-4 mr-2" }
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"bindings": { "children": "headerCopy.menu.static" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "card-content",
|
||||
"type": "CardContent",
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-or-content",
|
||||
"type": "ConditionalRender",
|
||||
"bindings": {
|
||||
"condition": {
|
||||
"source": "localSources",
|
||||
"transform": "localSources.length === 0"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "empty-state",
|
||||
"type": "EmptyState",
|
||||
"bindings": {
|
||||
"title": "emptyStateCopy.title",
|
||||
"description": "emptyStateCopy.description"
|
||||
},
|
||||
"props": {
|
||||
"icon": {
|
||||
"type": "PhosphorIcon",
|
||||
"props": { "icon": "Database", "className": "w-12 h-12" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sources-list",
|
||||
"type": "Stack",
|
||||
"props": { "direction": "vertical", "spacing": "xl" },
|
||||
"children": [
|
||||
{
|
||||
"id": "kv-section",
|
||||
"type": "Section",
|
||||
"bindings": {
|
||||
"hidden": {
|
||||
"source": "groupedSources",
|
||||
"transform": "groupedSources.kv.length === 0"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "IconText",
|
||||
"props": { "className": "text-sm font-semibold mb-3" },
|
||||
"children": [
|
||||
{
|
||||
"type": "PhosphorIcon",
|
||||
"props": { "icon": "Database", "className": "w-4 h-4" }
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "groupedSources",
|
||||
"transform": "`${headerCopy.groups.kv} (${groupedSources.kv.length})`"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Stack",
|
||||
"props": { "direction": "vertical", "spacing": "sm" },
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "groupedSources",
|
||||
"transform": "groupedSources.kv.map((ds) => ({ id: ds.id, type: 'kv', item: ds }))"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "static-section",
|
||||
"type": "Section",
|
||||
"bindings": {
|
||||
"hidden": {
|
||||
"source": "groupedSources",
|
||||
"transform": "groupedSources.static.length === 0"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "IconText",
|
||||
"props": { "className": "text-sm font-semibold mb-3" },
|
||||
"children": [
|
||||
{
|
||||
"type": "PhosphorIcon",
|
||||
"props": { "icon": "FileText", "className": "w-4 h-4" }
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "groupedSources",
|
||||
"transform": "`${headerCopy.groups.static} (${groupedSources.static.length})`"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Stack",
|
||||
"props": { "direction": "vertical", "spacing": "sm" },
|
||||
"bindings": {
|
||||
"children": {
|
||||
"source": "groupedSources",
|
||||
"transform": "groupedSources.static.map((ds) => ({ id: ds.id, type: 'static', item: ds }))"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "editor-dialog",
|
||||
"type": "DataSourceEditorDialog",
|
||||
"bindings": {
|
||||
"open": "dialogOpen",
|
||||
"dataSource": "editingSource",
|
||||
"onOpenChange": {
|
||||
"source": "setDialogOpen",
|
||||
"transform": "setDialogOpen"
|
||||
},
|
||||
"onSave": {
|
||||
"source": "updateDataSource",
|
||||
"transform": "(source) => updateDataSource(source.id, source)"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { DataSourceEditorDialog } from '@/lib/json-ui/json-components'
|
||||
import { useDataSourceManager } from '@/hooks/data/use-data-source-manager'
|
||||
import { DataSource, DataSourceType } from '@/types/json-ui'
|
||||
import { Database, FileText } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import { EmptyState, Stack } from '@/components/atoms'
|
||||
import { DataSourceManagerHeader } from '@/components/organisms/data-source-manager/DataSourceManagerHeader'
|
||||
import { DataSourceGroupSection } from '@/components/organisms/data-source-manager/DataSourceGroupSection'
|
||||
import dataSourceManagerCopy from '@/data/data-source-manager.json'
|
||||
|
||||
interface DataSourceManagerProps {
|
||||
dataSources: DataSource[]
|
||||
onChange: (dataSources: DataSource[]) => void
|
||||
}
|
||||
|
||||
export function DataSourceManager({ dataSources, onChange }: DataSourceManagerProps) {
|
||||
const {
|
||||
dataSources: localSources,
|
||||
addDataSource,
|
||||
updateDataSource,
|
||||
deleteDataSource,
|
||||
getDependents,
|
||||
} = useDataSourceManager(dataSources)
|
||||
|
||||
const [editingSource, setEditingSource] = useState<DataSource | null>(null)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
|
||||
const handleAddDataSource = (type: DataSourceType) => {
|
||||
const newSource = addDataSource(type)
|
||||
setEditingSource(newSource)
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleEditSource = (id: string) => {
|
||||
const source = localSources.find(ds => ds.id === id)
|
||||
if (source) {
|
||||
setEditingSource(source)
|
||||
setDialogOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteSource = (id: string) => {
|
||||
const dependents = getDependents(id)
|
||||
if (dependents.length > 0) {
|
||||
const noun = dependents.length === 1 ? 'source' : 'sources'
|
||||
toast.error(dataSourceManagerCopy.toasts.deleteBlockedTitle, {
|
||||
description: dataSourceManagerCopy.toasts.deleteBlockedDescription
|
||||
.replace('{count}', String(dependents.length))
|
||||
.replace('{noun}', noun),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
deleteDataSource(id)
|
||||
onChange(localSources.filter(ds => ds.id !== id))
|
||||
toast.success(dataSourceManagerCopy.toasts.deleted)
|
||||
}
|
||||
|
||||
const handleSaveSource = (updatedSource: DataSource) => {
|
||||
updateDataSource(updatedSource.id, updatedSource)
|
||||
onChange(localSources.map(ds => ds.id === updatedSource.id ? updatedSource : ds))
|
||||
toast.success(dataSourceManagerCopy.toasts.updated)
|
||||
}
|
||||
|
||||
const groupedSources = {
|
||||
kv: localSources.filter(ds => ds.type === 'kv'),
|
||||
static: localSources.filter(ds => ds.type === 'static'),
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<DataSourceManagerHeader
|
||||
copy={{
|
||||
title: dataSourceManagerCopy.header.title,
|
||||
description: dataSourceManagerCopy.header.description,
|
||||
addLabel: dataSourceManagerCopy.actions.add,
|
||||
menu: dataSourceManagerCopy.menu,
|
||||
}}
|
||||
onAdd={handleAddDataSource}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{localSources.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<Database size={48} weight="duotone" />}
|
||||
title={dataSourceManagerCopy.emptyState.title}
|
||||
description={dataSourceManagerCopy.emptyState.description}
|
||||
/>
|
||||
) : (
|
||||
<Stack direction="vertical" spacing="xl">
|
||||
<DataSourceGroupSection
|
||||
icon={<Database size={16} />}
|
||||
label={dataSourceManagerCopy.groups.kv}
|
||||
dataSources={groupedSources.kv}
|
||||
getDependents={getDependents}
|
||||
onEdit={handleEditSource}
|
||||
onDelete={handleDeleteSource}
|
||||
/>
|
||||
|
||||
<DataSourceGroupSection
|
||||
icon={<FileText size={16} />}
|
||||
label={dataSourceManagerCopy.groups.static}
|
||||
dataSources={groupedSources.static}
|
||||
getDependents={getDependents}
|
||||
onEdit={handleEditSource}
|
||||
onDelete={handleDeleteSource}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<DataSourceEditorDialog
|
||||
open={dialogOpen}
|
||||
dataSource={editingSource}
|
||||
onOpenChange={setDialogOpen}
|
||||
onSave={handleSaveSource}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
import { IconText, Section, Stack } from '@/components/atoms'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface DataSourceGroupSectionProps {
|
||||
icon: ReactNode
|
||||
label: string
|
||||
dataSources: DataSource[]
|
||||
getDependents: (id: string) => string[]
|
||||
onEdit: (id: string) => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
export function DataSourceGroupSection({
|
||||
icon,
|
||||
label,
|
||||
dataSources,
|
||||
getDependents,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: DataSourceGroupSectionProps) {
|
||||
if (dataSources.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<IconText
|
||||
icon={icon}
|
||||
className="text-sm font-semibold mb-3"
|
||||
>
|
||||
{label} ({dataSources.length})
|
||||
</IconText>
|
||||
<Stack direction="vertical" spacing="sm">
|
||||
{dataSources.map(ds => (
|
||||
<div
|
||||
key={ds.id}
|
||||
className="p-3 border rounded-md hover:bg-gray-50"
|
||||
>
|
||||
<div className="font-medium text-sm">{ds.id}</div>
|
||||
<button
|
||||
onClick={() => onEdit(ds.id)}
|
||||
className="text-xs text-blue-600 hover:underline mr-2"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDelete(ds.id)}
|
||||
className="text-xs text-red-600 hover:underline"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { ActionButton, Heading, Stack, Text } from '@/components/atoms'
|
||||
import { Plus, Database, FileText } from '@phosphor-icons/react'
|
||||
import { DataSourceType } from '@/types/json-ui'
|
||||
|
||||
interface DataSourceManagerHeaderCopy {
|
||||
title: string
|
||||
description: string
|
||||
addLabel: string
|
||||
menu: {
|
||||
kv: string
|
||||
static: string
|
||||
}
|
||||
}
|
||||
|
||||
interface DataSourceManagerHeaderProps {
|
||||
copy: DataSourceManagerHeaderCopy
|
||||
onAdd: (type: DataSourceType) => void
|
||||
}
|
||||
|
||||
export function DataSourceManagerHeader({ copy, onAdd }: DataSourceManagerHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<Stack direction="vertical" spacing="xs">
|
||||
<Heading level={2}>{copy.title}</Heading>
|
||||
<Text variant="muted">
|
||||
{copy.description}
|
||||
</Text>
|
||||
</Stack>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div>
|
||||
<ActionButton
|
||||
icon={<Plus size={16} />}
|
||||
label={copy.addLabel}
|
||||
variant="default"
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onAdd('kv')}>
|
||||
<Database className="w-4 h-4 mr-2" />
|
||||
{copy.menu.kv}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onAdd('static')}>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
{copy.menu.static}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
export { NavigationMenu } from './NavigationMenu'
|
||||
export { AppHeader } from './AppHeader'
|
||||
export { DataSourceManager } from './DataSourceManager'
|
||||
export { TreeListPanel } from './TreeListPanel'
|
||||
export { SchemaEditorLayout } from './SchemaEditorLayout'
|
||||
export { SchemaCodeViewer } from './SchemaCodeViewer'
|
||||
|
||||
95
src/hooks/use-data-source-manager-state.ts
Normal file
95
src/hooks/use-data-source-manager-state.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { DataSource, DataSourceType } from '@/types/json-ui'
|
||||
import { toast } from 'sonner'
|
||||
import dataSourceManagerCopy from '@/data/data-source-manager.json'
|
||||
|
||||
interface UseDataSourceManagerStateReturn {
|
||||
localSources: DataSource[]
|
||||
editingSource: DataSource | null
|
||||
dialogOpen: boolean
|
||||
groupedSources: {
|
||||
kv: DataSource[]
|
||||
static: DataSource[]
|
||||
}
|
||||
addDataSource: (type: DataSourceType) => void
|
||||
updateDataSource: (id: string, updates: Partial<DataSource>) => void
|
||||
deleteDataSource: (id: string) => void
|
||||
getDependents: (sourceId: string) => DataSource[]
|
||||
setEditingSource: (source: DataSource | null) => void
|
||||
setDialogOpen: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function useDataSourceManagerState(
|
||||
dataSources: DataSource[],
|
||||
onChange: (dataSources: DataSource[]) => void
|
||||
): UseDataSourceManagerStateReturn {
|
||||
const [localSources, setLocalSources] = useState<DataSource[]>(dataSources)
|
||||
const [editingSource, setEditingSource] = useState<DataSource | null>(null)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
|
||||
const getDependents = useCallback((sourceId: string) => {
|
||||
return localSources.filter(ds =>
|
||||
ds.dependencies?.includes(sourceId)
|
||||
)
|
||||
}, [localSources])
|
||||
|
||||
const addDataSource = useCallback((type: DataSourceType) => {
|
||||
const newSource: DataSource = {
|
||||
id: `ds-${Date.now()}`,
|
||||
type,
|
||||
...(type === 'kv' && { key: '', defaultValue: null }),
|
||||
...(type === 'static' && { defaultValue: null }),
|
||||
}
|
||||
setLocalSources(prev => [...prev, newSource])
|
||||
setEditingSource(newSource)
|
||||
setDialogOpen(true)
|
||||
}, [])
|
||||
|
||||
const updateDataSource = useCallback((id: string, updates: Partial<DataSource>) => {
|
||||
const updated = localSources.map(ds =>
|
||||
ds.id === id ? { ...ds, ...updates } : ds
|
||||
)
|
||||
setLocalSources(updated)
|
||||
onChange(updated)
|
||||
toast.success(dataSourceManagerCopy.toasts.updated)
|
||||
}, [localSources, onChange])
|
||||
|
||||
const deleteDataSource = useCallback((id: string) => {
|
||||
const dependents = localSources.filter(ds =>
|
||||
ds.dependencies?.includes(id)
|
||||
)
|
||||
|
||||
if (dependents.length > 0) {
|
||||
const noun = dependents.length === 1 ? 'source' : 'sources'
|
||||
toast.error(dataSourceManagerCopy.toasts.deleteBlockedTitle, {
|
||||
description: dataSourceManagerCopy.toasts.deleteBlockedDescription
|
||||
.replace('{count}', String(dependents.length))
|
||||
.replace('{noun}', noun),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const updated = localSources.filter(ds => ds.id !== id)
|
||||
setLocalSources(updated)
|
||||
onChange(updated)
|
||||
toast.success(dataSourceManagerCopy.toasts.deleted)
|
||||
}, [localSources, onChange])
|
||||
|
||||
const groupedSources = {
|
||||
kv: localSources.filter(ds => ds.type === 'kv'),
|
||||
static: localSources.filter(ds => ds.type === 'static'),
|
||||
}
|
||||
|
||||
return {
|
||||
localSources,
|
||||
editingSource,
|
||||
dialogOpen,
|
||||
groupedSources,
|
||||
addDataSource,
|
||||
updateDataSource,
|
||||
deleteDataSource,
|
||||
getDependents,
|
||||
setEditingSource,
|
||||
setDialogOpen,
|
||||
}
|
||||
}
|
||||
6
src/lib/json-ui/interfaces/data-source-manager.ts
Normal file
6
src/lib/json-ui/interfaces/data-source-manager.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { DataSource, DataSourceType } from '@/types/json-ui'
|
||||
|
||||
export interface DataSourceManagerProps {
|
||||
dataSources: DataSource[]
|
||||
onChange: (dataSources: DataSource[]) => void
|
||||
}
|
||||
@@ -332,6 +332,10 @@ export const jsonUIComponentTypes = [
|
||||
"Upload",
|
||||
"User",
|
||||
"X",
|
||||
"AppLayout",
|
||||
"AppRouterLayout",
|
||||
"AppMainPanel",
|
||||
"AppDialogs",
|
||||
] as const
|
||||
|
||||
export type JSONUIComponentType = typeof jsonUIComponentTypes[number]
|
||||
|
||||
Reference in New Issue
Block a user