- {kindOrder.map(kind => {
- const group = groupedTypes[kind];
- if (!group || group.length === 0) return null;
-
- return (
-
-
- {kind}
-
-
- {group.map(t => (
-
onSelect(t)}
- className={`w-full text-left px-2 py-1.5 rounded-md text-xs transition-colors flex items-center justify-between ${selectedTypeId === t.id
- ? 'bg-secondary text-secondary-foreground font-medium'
- : 'hover:bg-muted text-muted-foreground'
- }`}
- >
- {t.name}
- {selectedTypeId === t.id && (
-
- )}
-
- ))}
-
-
- );
- })}
+
+
(
+ onSelect(t)}
+ className={`w-full text-left px-2 py-1.5 rounded-md text-xs transition-colors flex items-center justify-between ${
+ selectedTypeId === t.id
+ ? 'bg-secondary text-secondary-foreground font-medium'
+ : 'hover:bg-muted text-muted-foreground'
+ }`}
+ >
+ {t.name}
+ {selectedTypeId === t.id && (
+
+ )}
+
+ )}
+ />
diff --git a/packages/ui/src/modules/types/builder/TypeBuilderContent.tsx b/packages/ui/src/modules/types/builder/TypeBuilderContent.tsx
index 7fde9568..30c071a6 100644
--- a/packages/ui/src/modules/types/builder/TypeBuilderContent.tsx
+++ b/packages/ui/src/modules/types/builder/TypeBuilderContent.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { useDroppable } from '@dnd-kit/core';
+import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -12,6 +13,7 @@ import { Box } from 'lucide-react';
import { CategoryManager } from '@/components/widgets/CategoryManager';
import { BuilderMode, BuilderElement, BuilderOutput, EnumValueEntry, FlagValueEntry } from './types';
import { DraggablePaletteItem, CanvasElement, WidgetPicker } from './components';
+import { TypeCategoryTree } from '../TypeCategoryTree';
import { EnumEditor, FlagsEditor } from './Editors';
import { resolvePrimitiveType } from './utils';
import { TypeDefinition } from '../client-types';
@@ -54,41 +56,7 @@ export const TypeBuilderContent: React.FC<{
id: 'canvas',
});
- const primitivePaletteItems = React.useMemo(() => {
- const order = ['string', 'int', 'float', 'bool', 'object', 'array'];
- return availableTypes
- .filter(t => t.kind === 'primitive')
- .sort((a, b) => {
- const ia = order.indexOf(a.name);
- const ib = order.indexOf(b.name);
- if (ia !== -1 && ib !== -1) return ia - ib;
- if (ia !== -1) return -1;
- if (ib !== -1) return 1;
- return a.name.localeCompare(b.name);
- })
- .map(t => ({
- id: `primitive-${t.id}`,
- type: t.name,
- name: t.name.charAt(0).toUpperCase() + t.name.slice(1),
- title: `New ${t.name.charAt(0).toUpperCase() + t.name.slice(1)}`,
- description: t.description || undefined,
- refId: t.id
- } as BuilderElement & { refId?: string }));
- }, [availableTypes]);
-
- const customPaletteItems = React.useMemo(() => {
- return availableTypes
- .filter(t => t.kind !== 'primitive' && t.kind !== 'field')
- .map(t => ({
- id: `type-${t.id}`,
- type: t.name,
- name: t.name,
- title: t.name,
- description: t.description || undefined,
- isCustom: true,
- refId: t.id
- } as BuilderElement & { isCustom?: boolean, refId?: string }));
- }, [availableTypes]);
+ // We don't pre-map to Palette items here anymore, as TypeCategoryTree handles TypeDefinitions directly.
return (
@@ -98,28 +66,47 @@ export const TypeBuilderContent: React.FC<{
Palette
-
-
Primitives
-
- {primitivePaletteItems.length > 0 ? (
- primitivePaletteItems.map(item => (
-
- ))
- ) : (
-
No primitive types found.
- )}
-
-
-
- {customPaletteItems.length > 0 && mode !== 'alias' && (
-
-
Custom Types
-
- {customPaletteItems.map(item => (
-
- ))}
-
-
+ {mode !== 'alias' ? (
+ {
+ const isPrimitive = t.kind === 'primitive';
+ const item: BuilderElement = isPrimitive ? {
+ id: `primitive-${t.id}`,
+ type: t.name,
+ name: t.name.charAt(0).toUpperCase() + t.name.slice(1),
+ title: `New ${t.name.charAt(0).toUpperCase() + t.name.slice(1)}`,
+ description: t.description || undefined,
+ refId: t.id
+ } : {
+ id: `type-${t.id}`,
+ type: t.name,
+ name: t.name,
+ title: t.name,
+ description: t.description || undefined,
+ isCustom: true,
+ refId: t.id
+ } as BuilderElement & { isCustom?: boolean, refId?: string };
+ return ;
+ }}
+ />
+ ) : (
+ t.kind === 'primitive')}
+ excludeFieldTypes={true}
+ renderItem={(t) => {
+ const item: BuilderElement = {
+ id: `primitive-${t.id}`,
+ type: t.name,
+ name: t.name.charAt(0).toUpperCase() + t.name.slice(1),
+ title: `New ${t.name.charAt(0).toUpperCase() + t.name.slice(1)}`,
+ description: t.description || undefined,
+ refId: t.id
+ };
+ return ;
+ }}
+ />
)}
@@ -169,16 +156,18 @@ export const TypeBuilderContent: React.FC<{
) : (
- {elements.map(el => (
- setSelectedId(el.id)}
- onDelete={() => deleteElement(el.id)}
- onRemoveOnly={() => removeElement(el.id)}
- />
- ))}
+ e.id)} strategy={verticalListSortingStrategy}>
+ {elements.map(el => (
+ setSelectedId(el.id)}
+ onDelete={() => deleteElement(el.id)}
+ onRemoveOnly={() => removeElement(el.id)}
+ />
+ ))}
+
)}
>
@@ -238,8 +227,8 @@ export const TypeBuilderContent: React.FC<{
- {primitivePaletteItems.map(p => (
- {p.name}
+ {availableTypes.filter(t => t.kind === 'primitive').map(p => (
+ {p.name.charAt(0).toUpperCase() + p.name.slice(1)}
))}
@@ -329,6 +318,16 @@ export const TypeBuilderContent: React.FC<{
The initial value for this field.
+
Array Configuration
diff --git a/packages/ui/src/modules/types/builder/components.tsx b/packages/ui/src/modules/types/builder/components.tsx
index 902961e6..d28b3002 100644
--- a/packages/ui/src/modules/types/builder/components.tsx
+++ b/packages/ui/src/modules/types/builder/components.tsx
@@ -1,5 +1,7 @@
import React, { useState } from 'react';
import { useDraggable } from '@dnd-kit/core';
+import { useSortable } from '@dnd-kit/sortable';
+import { CSS } from '@dnd-kit/utilities';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
@@ -50,16 +52,37 @@ export const CanvasElement = ({
const primitiveTypes = ['string', 'number', 'int', 'float', 'boolean', 'bool', 'array', 'object'];
const isPrimitive = primitiveTypes.includes(element.type.toLowerCase());
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ } = useSortable({ id: element.id, data: element });
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ };
+
return (
{ e.stopPropagation(); onSelect(); }}
className={`p-3 border rounded-md mb-2 cursor-pointer flex items-center justify-between group ${isSelected ? 'ring-2 ring-primary border-primary' : 'hover:border-primary/50 bg-background'}`}
>
+
+
+
{element.title || element.name}
- {element.type}
+
+ {element.type}
+ {element.group && {element.group} }
+
diff --git a/packages/ui/src/modules/types/builder/types.ts b/packages/ui/src/modules/types/builder/types.ts
index ec810ba1..8de76dfc 100644
--- a/packages/ui/src/modules/types/builder/types.ts
+++ b/packages/ui/src/modules/types/builder/types.ts
@@ -10,6 +10,7 @@ export interface BuilderElement {
refId?: string;
required?: boolean;
defaultValue?: any;
+ group?: string;
}
export type BuilderMode = 'structure' | 'alias' | 'enum' | 'flags';
diff --git a/packages/ui/src/modules/types/builder/utils.ts b/packages/ui/src/modules/types/builder/utils.ts
index 9467e016..88b85704 100644
--- a/packages/ui/src/modules/types/builder/utils.ts
+++ b/packages/ui/src/modules/types/builder/utils.ts
@@ -1,7 +1,8 @@
import { Type as TypeIcon, Hash, ToggleLeft, Box, List, FileJson } from 'lucide-react';
import { TypeDefinition } from '../client-types';
-export function getIconForType(type: string) {
+export function getIconForType(type: string | undefined) {
+ if (!type) return FileJson;
switch (type.toLowerCase()) {
case 'string': return TypeIcon;
case 'int':
diff --git a/packages/ui/src/modules/types/schema-utils.ts b/packages/ui/src/modules/types/schema-utils.ts
index a4161c69..abca804a 100644
--- a/packages/ui/src/modules/types/schema-utils.ts
+++ b/packages/ui/src/modules/types/schema-utils.ts
@@ -25,6 +25,11 @@ export const generateSchemaForType = (typeId: string, types: TypeDefinition[], v
const type = types.find(t => t.id === typeId);
if (!type) return { type: 'string' };
+ // Field rows wrap the actual value type (enum, flags, …) — resolve through parent
+ if (type.kind === 'field' && type.parent_type_id) {
+ return generateSchemaForType(type.parent_type_id, types, visited);
+ }
+
// If it's a primitive, return the JSON schema mapping
if (type.kind === 'primitive') {
return primitiveToJsonSchema[type.name] || { type: 'string' };
@@ -137,7 +142,10 @@ export const generateUiSchemaForType = (typeId: string, types: TypeDefinition[],
const isArray = parentType?.name === 'array' || parentType?.kind === 'primitive' && parentType?.name === 'array';
const isEnum = parentType?.kind === 'enum';
const isFlags = parentType?.kind === 'flags';
- const fieldUiSchema = fieldType?.meta?.uiSchema || {};
+ const fieldUiSchema = {
+ ...(fieldType?.meta?.uiSchema || {}),
+ ...(fieldType?.settings?.group && { 'ui:group': fieldType.settings.group })
+ };
if (isEnum) {
// Enum field — default to select widget if not overridden