From 81dae7a5c5873c434b5d0f0ed3c5ee0639c241ff Mon Sep 17 00:00:00 2001 From: Babayaga Date: Sat, 4 Apr 2026 01:15:16 +0200 Subject: [PATCH] types:vfs/cats/groups/posts/pages - cleanup --- .../components/admin/ProductTypeDataForms.tsx | 25 +- .../pages/editor/UserPageTypeFields.tsx | 38 +-- ...ppPickerWidgets.tsx => AppTypeWidgets.tsx} | 288 +++++++++++++++++- packages/ui/src/modules/types/ImageWidget.tsx | 134 -------- packages/ui/src/modules/types/RJSFForm.tsx | 27 ++ .../ui/src/modules/types/RJSFTemplates.tsx | 137 ++++++++- packages/ui/src/modules/types/TypeForm.tsx | 57 ++++ .../ui/src/modules/types/TypeRenderer.tsx | 39 +-- packages/ui/src/modules/types/TypesEditor.tsx | 2 +- packages/ui/src/modules/types/TypesList.tsx | 9 +- .../ui/src/modules/types/builder/Editors.tsx | 1 + .../types/{ => builder}/TypeBuilder.tsx | 89 ++---- .../types/builder/TypeBuilderContent.tsx | 45 ++- .../types/builder/appPickerWidgetOptions.ts | 48 --- .../src/modules/types/builder/components.tsx | 42 ++- .../ui/src/modules/types/builder/useStore.ts | 94 ++++++ .../ui/src/modules/types/builder/utils.ts | 2 +- .../modules/types/categoryPickerWidget.tsx | 39 --- .../src/modules/types/groupPickerWidget.tsx | 39 --- .../src/modules/types/randomDataGenerator.ts | 28 +- .../src/modules/types/rjsfButtonTemplates.tsx | 130 -------- .../src/modules/types/rjsfWidgetRegistry.ts | 6 +- packages/ui/src/modules/types/schema-utils.ts | 14 +- .../ui/src/modules/types/userPickerWidget.tsx | 38 --- 24 files changed, 735 insertions(+), 636 deletions(-) rename packages/ui/src/modules/types/{appPickerWidgets.tsx => AppTypeWidgets.tsx} (53%) delete mode 100644 packages/ui/src/modules/types/ImageWidget.tsx create mode 100644 packages/ui/src/modules/types/RJSFForm.tsx create mode 100644 packages/ui/src/modules/types/TypeForm.tsx rename packages/ui/src/modules/types/{ => builder}/TypeBuilder.tsx (58%) delete mode 100644 packages/ui/src/modules/types/builder/appPickerWidgetOptions.ts create mode 100644 packages/ui/src/modules/types/builder/useStore.ts delete mode 100644 packages/ui/src/modules/types/categoryPickerWidget.tsx delete mode 100644 packages/ui/src/modules/types/groupPickerWidget.tsx delete mode 100644 packages/ui/src/modules/types/rjsfButtonTemplates.tsx delete mode 100644 packages/ui/src/modules/types/userPickerWidget.tsx diff --git a/packages/ui/src/components/admin/ProductTypeDataForms.tsx b/packages/ui/src/components/admin/ProductTypeDataForms.tsx index 6eba80b3..4f109c3f 100644 --- a/packages/ui/src/components/admin/ProductTypeDataForms.tsx +++ b/packages/ui/src/components/admin/ProductTypeDataForms.tsx @@ -1,9 +1,5 @@ import React, { useMemo } from 'react'; -import Form from '@rjsf/core'; -import validator from '@rjsf/validator-ajv8'; -import { rjsfWidgetRegistry } from '@/modules/types/rjsfWidgetRegistry'; -import { customTemplates } from '@/modules/types/RJSFTemplates'; -import { generateSchemaForType, generateUiSchemaForType, deepMergeUiSchema } from '@/modules/types/schema-utils'; +import { TypeForm } from '@/modules/types/TypeForm'; import type { TypeDefinition } from '@/modules/types/client-types'; import { Label } from '@/components/ui/label'; import { T } from '@/i18n'; @@ -23,25 +19,16 @@ const SingleTypeForm = ({ onChange: (fd: Record) => void; disabled?: boolean; }) => { - const jsonSchema = useMemo(() => generateSchemaForType(typeId, types), [typeId, types]); - const uiSchema = useMemo(() => { - const gen = generateUiSchemaForType(typeId, types); - return deepMergeUiSchema(gen, typeDef.meta?.uiSchema || {}); - }, [typeId, types, typeDef.meta?.uiSchema]); - return (
-
onChange((fd || {}) as Record)} > . - +
); }; @@ -57,7 +44,7 @@ const SingleTypeForm = ({ export interface ProductTypeDataFormsProps { /** Structure (and other) type ids configured for this product */ typeIds: string[]; - /** Full type registry from `fetchTypes()` — required so `generateSchemaForType` can resolve fields (enums, primitives, field rows, nested structures). */ + /** Full type registry from `fetchTypes()` — required so `TypeForm` can parse and resolve schemas. */ types: TypeDefinition[]; /** Per-type id → form payload (structure field values) */ value: Record>; diff --git a/packages/ui/src/modules/pages/editor/UserPageTypeFields.tsx b/packages/ui/src/modules/pages/editor/UserPageTypeFields.tsx index 7719a745..f2259080 100644 --- a/packages/ui/src/modules/pages/editor/UserPageTypeFields.tsx +++ b/packages/ui/src/modules/pages/editor/UserPageTypeFields.tsx @@ -1,10 +1,6 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import Form from '@rjsf/core'; -import validator from '@rjsf/validator-ajv8'; +import React, { useState, useEffect } from 'react'; +import { TypeForm } from '@/modules/types/TypeForm'; import { TypeDefinition, fetchTypes } from '@/modules/types/client-types'; -import { generateSchemaForType, generateUiSchemaForType, deepMergeUiSchema } from '@/modules/types/schema-utils'; -import { rjsfWidgetRegistry } from '@/modules/types/rjsfWidgetRegistry'; -import { customTemplates } from '@/modules/types/RJSFTemplates'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { T, translate } from '@/i18n'; @@ -125,23 +121,6 @@ export const UserPageTypeFields: React.FC = ({ t.id)} key={assignedTypes.map(t => t.id).join(',')}> {assignedTypes.map(type => { - const schema = generateSchemaForType(type.id, allTypes); - // Ensure schema is object type for form rendering - if (schema.type !== 'object') { - return null; - } - - const uiSchema = generateUiSchemaForType(type.id, allTypes); - - // Merge with type's own UI schema but FORCE our single-column grid layout - // Merge with type's own UI schema but FORCE our single-column grid layout - // Use deep merge to preserve field settings - const mergedUiSchema = deepMergeUiSchema(uiSchema, type.meta?.uiSchema || {}); - const finalUiSchema = { - ...mergedUiSchema, - 'ui:classNames': 'grid grid-cols-1 gap-y-1' - }; - const typeData = formData[type.id] || {}; return ( @@ -180,20 +159,19 @@ export const UserPageTypeFields: React.FC = ({ -
isEditMode && handleFormChange(type.id, e.formData)} readonly={!isEditMode} className={isEditMode ? "" : "pointer-events-none opacity-80"} > {/* No submit button — changes are saved via the global save (Ctrl+S) */} <> -
+
); diff --git a/packages/ui/src/modules/types/appPickerWidgets.tsx b/packages/ui/src/modules/types/AppTypeWidgets.tsx similarity index 53% rename from packages/ui/src/modules/types/appPickerWidgets.tsx rename to packages/ui/src/modules/types/AppTypeWidgets.tsx index 25124e34..b374dbb1 100644 --- a/packages/ui/src/modules/types/appPickerWidgets.tsx +++ b/packages/ui/src/modules/types/AppTypeWidgets.tsx @@ -3,16 +3,23 @@ * ui:widget keys are listed in builder/appPickerWidgetOptions.ts. */ import React, { lazy, Suspense, useEffect, useState } from 'react'; +import { GroupPicker } from '@/components/admin/GroupPicker'; import type { WidgetProps } from '@rjsf/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { FileText, FolderOpen, Image as ImageIcon, X } from 'lucide-react'; +import { FileText, FolderOpen, Image as ImageIcon, X, RefreshCw } from 'lucide-react'; import { T, translate } from '@/i18n'; import { PagePickerDialog } from '@/modules/pages/PagePickerDialog'; import { fetchPageDetailsById } from '@/modules/pages/client-pages'; import PostPicker from '@/components/PostPicker'; import { fetchPostDetailsAPI } from '@/modules/posts/client-posts'; +import { CategoryPickerField } from '@/components/widgets/CategoryPickerField'; +import { ImagePickerDialog } from '@/components/widgets/ImagePickerDialog'; +import { fetchPictureById } from '@/modules/posts/client-pictures'; +import { UserPicker } from '@/components/admin/UserPicker'; + +import { getUiOptions } from '@rjsf/utils'; const FileBrowserWidget = lazy(() => import('@/modules/storage/FileBrowserWidget').then((m) => ({ default: m.default })) @@ -336,3 +343,282 @@ export const FilePickerWidget = (props: WidgetProps) => { ); }; + +type CategoryUiOptions = { + filterType?: string; +}; + +/** + * RJSF widget wrapping CategoryPickerField. Stores selected category id (string). + * Optional `ui:options.filterType` matches CategoryPickerField (e.g. "types", "pages"). + */ +export const CategoryPickerWidget = (props: WidgetProps) => { + const { id, value, disabled, readonly, onChange, onBlur, onFocus, schema, uiSchema } = props; + const uiOptions = getUiOptions(uiSchema) as CategoryUiOptions; + const filterType = uiOptions.filterType ?? (schema as { 'x-category-filter-type'?: string })?.['x-category-filter-type']; + + const v = value == null ? '' : String(value); + + if (readonly || disabled) { + return ( +
onBlur(id, value)} + onFocus={() => onFocus(id, value)} + > + {v || '—'} +
+ ); + } + + return ( +
onBlur(id, value)} onFocus={() => onFocus(id, value)}> + onChange(categoryId || undefined)} filterType={filterType} /> +
+ ); +}; + +/** + * RJSF widget wrapping GroupPicker (single). Stores selected group name (string), matching GroupPicker onSelect. + */ +export const GroupPickerWidget = (props: WidgetProps) => { + const { id, value, disabled, readonly, onChange, onBlur, onFocus } = props; + const v = value == null ? '' : String(value); + + if (readonly) { + return ( +
onBlur(id, value)} + onFocus={() => onFocus(id, value)} + > + {v || '—'} +
+ ); + } + + return ( +
onBlur(id, value)} + onFocus={() => onFocus(id, value)} + > + onChange(groupName || undefined)} + /> +
+ ); +}; + +export const ImageWidget = (props: WidgetProps) => { + const { + id, + value, + readonly, + disabled, + onChange, + onBlur, + onFocus, + } = props; + + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [picture, setPicture] = useState(null); + const [loading, setLoading] = useState(false); + + // Fetch picture details if value exists + useEffect(() => { + const fetchPicture = async () => { + if (!value) { + setPicture(null); + return; + } + + // If we already have the picture and IDs match, don't re-fetch + if (picture && picture.id === value) return; + + setLoading(true); + try { + const data = await fetchPictureById(value); + setPicture(data); + } catch (error) { + console.error("Error fetching picture:", error); + } finally { + setLoading(false); + } + }; + + fetchPicture(); + }, [value]); + + const handleSelectPicture = (selectedPicture: any) => { + onChange(selectedPicture.id); + setPicture(selectedPicture); + setIsDialogOpen(false); + }; + + const handleClear = () => { + onChange(undefined); + setPicture(null); + }; + + return ( +
+ {!value ? ( + + ) : ( +
+
+ {loading ? ( +
+ +
+ ) : picture ? ( +
+ {picture.title +
+ {picture.title} +
+
+ ) : ( +
+ Image ID: {value} (Loading or Not Found) +
+ )} +
+ + {/* Actions Overlay */} + {!readonly && !disabled && ( +
+ + +
+ )} +
+ )} + + setIsDialogOpen(false)} + onSelectPicture={handleSelectPicture} + currentValue={value} + /> +
+ ); +}; + +/** + * RJSF widget wrapping UserPicker. Stores selected user id (string). + */ +export const UserPickerWidget = (props: WidgetProps) => { + const { id, value, disabled, readonly, onChange, onBlur, onFocus } = props; + const v = value == null ? '' : String(value); + + if (readonly) { + return ( +
onBlur(id, value)} + onFocus={() => onFocus(id, value)} + > + {v || '—'} +
+ ); + } + + return ( +
onBlur(id, value)} + onFocus={() => onFocus(id, value)} + > + onChange(userId || undefined)} + /> +
+ ); +}; + +// --- Widget Group Options --- + +/** What is persisted in form data for each ui:widget (for authors / codegen). */ +export const APP_PICKER_STORAGE_HINT: Record = { + pagePicker: 'Page UUID', + postPicker: 'Post UUID', + filePicker: 'VFS location as mount:path or mount:dir/file.ext', + categoryPicker: 'Category UUID', + userPicker: 'User id (UUID)', + groupPicker: 'ACL group name (string)', +}; + +export const APP_PICKER_WIDGET_GROUPS = [ + { + label: 'App pickers', + options: [ + { value: 'pagePicker', label: 'Page (ID)', types: ['string'] as const }, + { value: 'postPicker', label: 'Post (ID)', types: ['string'] as const }, + { value: 'filePicker', label: 'VFS file / path', types: ['string'] as const }, + ], + }, +] as const; + +/** Drag palette + default JSON keys when dropping onto a structure */ +export const APP_PICKER_PALETTE_ENTRIES = [ + { widget: 'pagePicker' as const, defaultFieldName: 'pageId', paletteTitle: 'Page (ID)' }, + { widget: 'postPicker' as const, defaultFieldName: 'postId', paletteTitle: 'Post (ID)' }, + { widget: 'filePicker' as const, defaultFieldName: 'filePath', paletteTitle: 'VFS file / path' }, +] as const; + +/** + * Array field "Items Type" — string items plus a per-item ui:widget (same widgets as single-field pickers). + */ +export const ARRAY_ITEMS_APP_PICKER_ENTRIES = [ + { widget: 'categoryPicker' as const, label: 'Category' }, + { widget: 'userPicker' as const, label: 'User' }, + { widget: 'groupPicker' as const, label: 'Group' }, + { widget: 'pagePicker' as const, label: 'Page (ID)' }, + { widget: 'postPicker' as const, label: 'Post (ID)' }, + { widget: 'filePicker' as const, label: 'VFS file / path' }, +] as const; + +export const ARRAY_ITEMS_APP_PICKER_WIDGETS: ReadonlySet = new Set( + ARRAY_ITEMS_APP_PICKER_ENTRIES.map((e) => e.widget) +); diff --git a/packages/ui/src/modules/types/ImageWidget.tsx b/packages/ui/src/modules/types/ImageWidget.tsx deleted file mode 100644 index 6eb0e9d9..00000000 --- a/packages/ui/src/modules/types/ImageWidget.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { WidgetProps } from '@rjsf/utils'; -import { Button } from '@/components/ui/button'; -import { Image as ImageIcon, X, RefreshCw } from 'lucide-react'; -import { ImagePickerDialog } from '@/components/widgets/ImagePickerDialog'; -import { fetchPictureById } from '@/modules/posts/client-pictures'; - - -export const ImageWidget = (props: WidgetProps) => { - const { - id, - value, - readonly, - disabled, - onChange, - onBlur, - onFocus, - } = props; - - const [isDialogOpen, setIsDialogOpen] = useState(false); - const [picture, setPicture] = useState(null); - const [loading, setLoading] = useState(false); - - // Fetch picture details if value exists - useEffect(() => { - const fetchPicture = async () => { - if (!value) { - setPicture(null); - return; - } - - // If we already have the picture and IDs match, don't re-fetch - if (picture && picture.id === value) return; - - setLoading(true); - try { - const data = await fetchPictureById(value); - setPicture(data); - } catch (error) { - console.error("Error fetching picture:", error); - } finally { - setLoading(false); - } - }; - - fetchPicture(); - }, [value]); - - const handleSelectPicture = (selectedPicture: any) => { - onChange(selectedPicture.id); - setPicture(selectedPicture); - setIsDialogOpen(false); - }; - - const handleClear = () => { - onChange(undefined); - setPicture(null); - }; - - return ( -
- {!value ? ( - - ) : ( -
-
- {loading ? ( -
- -
- ) : picture ? ( -
- {picture.title -
- {picture.title} -
-
- ) : ( -
- Image ID: {value} (Loading or Not Found) -
- )} -
- - {/* Actions Overlay */} - {!readonly && !disabled && ( -
- - -
- )} -
- )} - - setIsDialogOpen(false)} - onSelectPicture={handleSelectPicture} - currentValue={value} - /> -
- ); -}; diff --git a/packages/ui/src/modules/types/RJSFForm.tsx b/packages/ui/src/modules/types/RJSFForm.tsx new file mode 100644 index 00000000..5c3270dc --- /dev/null +++ b/packages/ui/src/modules/types/RJSFForm.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import BaseForm, { FormProps } from '@rjsf/core'; +import validator from '@rjsf/validator-ajv8'; +import { customWidgets, customTemplates } from './RJSFTemplates'; + +/** + * A wrapper around `@rjsf/core` Form that pre-injects our standard validator, + * widget registry, and custom templates to avoid prop drilling everywhere. + */ +export const RJSFForm = ( + props: Omit, 'validator' | 'widgets' | 'templates'> & { + validator?: import('@rjsf/utils').ValidatorType; + widgets?: import('@rjsf/utils').RegistryWidgetsType; + templates?: Partial>; + } +) => { + return ( + + ); +}; + +export default RJSFForm; diff --git a/packages/ui/src/modules/types/RJSFTemplates.tsx b/packages/ui/src/modules/types/RJSFTemplates.tsx index f3fca773..e6a7f8bc 100644 --- a/packages/ui/src/modules/types/RJSFTemplates.tsx +++ b/packages/ui/src/modules/types/RJSFTemplates.tsx @@ -1,8 +1,135 @@ import React from 'react'; -import type { WidgetProps, RegistryWidgetsType, ArrayFieldTemplateProps, ArrayFieldItemTemplateProps } from '@rjsf/utils'; +import type { WidgetProps, RegistryWidgetsType, ArrayFieldTemplateProps, ArrayFieldItemTemplateProps, IconButtonProps } from '@rjsf/utils'; import { getTemplate, getUiOptions, type FormContextType, type RJSFSchema, type StrictRJSFSchema } from '@rjsf/utils'; import CollapsibleSection from '@/components/CollapsibleSection'; -import { rjsfButtonTemplateOverrides } from '@/modules/types/rjsfButtonTemplates'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { ChevronDown, ChevronUp, Copy, Plus, Trash2, X } from 'lucide-react'; + +const iconBtn = + 'h-7 w-7 shrink-0 p-0 text-muted-foreground hover:text-foreground hover:bg-muted'; + +/** + * RJSF core ships Bootstrap + glyphicon icon buttons; we don't load those styles, so controls were effectively invisible. + * These replace the array toolbar actions with shadcn + Lucide. + */ +function RjsfToolbarIconButton( + props: IconButtonProps & { + children: React.ReactNode; + destructive?: boolean; + } +) { + const { className, disabled, onClick, title, id, children, destructive, registry: _r, uiSchema: _u, icon: _i, iconType: _it } = props; + const label = typeof title === 'string' ? title : 'Action'; + return ( + + ); +} + +export function RjsfRemoveButton< + T = unknown, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = Record, +>(props: IconButtonProps) { + const { className, ...rest } = props; + return ( + + + + ); +} + +export function RjsfMoveUpButton< + T = unknown, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = Record, +>(props: IconButtonProps) { + const { className, ...rest } = props; + return ( + + + + ); +} + +export function RjsfMoveDownButton< + T = unknown, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = Record, +>(props: IconButtonProps) { + const { className, ...rest } = props; + return ( + + + + ); +} + +export function RjsfCopyButton< + T = unknown, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = Record, +>(props: IconButtonProps) { + const { className, ...rest } = props; + return ( + + + + ); +} + +/** Used for additionalProperties and other add affordances that still use the default AddButton slot. */ +export function RjsfAddIconButton< + T = unknown, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = Record, +>(props: IconButtonProps) { + const { className, ...rest } = props; + return ( + + + + ); +} + +export function RjsfClearButton< + T = unknown, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = Record, +>(props: IconButtonProps) { + const { className, ...rest } = props; + return ( + + + + ); +} + +/** Partial overrides merged with @rjsf/core defaults (see Form registry merge). */ +export const rjsfButtonTemplateOverrides = { + RemoveButton: RjsfRemoveButton, + MoveUpButton: RjsfMoveUpButton, + MoveDownButton: RjsfMoveDownButton, + CopyButton: RjsfCopyButton, + AddButton: RjsfAddIconButton, + ClearButton: RjsfClearButton, +}; // Utility function to convert camelCase to Title Case const formatLabel = (str: string): string => { @@ -349,11 +476,7 @@ export const ArrayFieldTemplate = (props: ArrayFieldTemplateProps) => { }; // Custom widgets -import { ImageWidget } from '@/modules/types/ImageWidget'; -import { FilePickerWidget, PagePickerWidget, PostPickerWidget } from '@/modules/types/appPickerWidgets'; -import { CategoryPickerWidget } from '@/modules/types/categoryPickerWidget'; -import { UserPickerWidget } from '@/modules/types/userPickerWidget'; -import { GroupPickerWidget } from '@/modules/types/groupPickerWidget'; +import { FilePickerWidget, PagePickerWidget, PostPickerWidget, CategoryPickerWidget, GroupPickerWidget, UserPickerWidget, ImageWidget } from '@/modules/types/AppTypeWidgets'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Checkbox as RadixCheckbox } from '@/components/ui/checkbox'; diff --git a/packages/ui/src/modules/types/TypeForm.tsx b/packages/ui/src/modules/types/TypeForm.tsx new file mode 100644 index 00000000..968135e7 --- /dev/null +++ b/packages/ui/src/modules/types/TypeForm.tsx @@ -0,0 +1,57 @@ +import React, { useMemo } from 'react'; +import { TypeDefinition } from '@/modules/types/client-types'; +import { generateSchemaForType, generateUiSchemaForType, deepMergeUiSchema } from '@/modules/types/schema-utils'; +import RJSFForm from '@/modules/types/RJSFForm'; + +export interface TypeFormProps extends Omit, 'schema' | 'uiSchema'> { + /** The definition of the type to render */ + typeDef: TypeDefinition; + /** The full registry of types, needed to resolve nested structures/enums */ + types: TypeDefinition[]; + /** Any additional uiSchema overrides to merge with the final generated uiSchema */ + uiSchemaOverrides?: Record; + /** + * If true, validation is skipped if the generated schema is not an object. + * Useful when we only want to render forms for objects (like in UserPageTypeFields). + */ + requireObjectSchema?: boolean; +} + +/** + * A higher-order wrapper around RJSFFormWrapper that automatically generates + * the JSON Schema and UI Schema from a Polymech `TypeDefinition`. + */ +export const TypeForm: React.FC = ({ + typeDef, + types, + uiSchemaOverrides, + requireObjectSchema, + ...formProps +}) => { + const jsonSchema = useMemo(() => generateSchemaForType(typeDef.id, types), [typeDef.id, types]); + + const uiSchema = useMemo(() => { + const generatedUi = generateUiSchemaForType(typeDef.id, types); + // Merge with the type's own UI schema metadata + let merged = deepMergeUiSchema(generatedUi, typeDef.meta?.uiSchema || {}); + // Merge with any explicit overrides provided as props + if (uiSchemaOverrides) { + merged = deepMergeUiSchema(merged, uiSchemaOverrides); + } + return merged; + }, [typeDef.id, types, typeDef.meta?.uiSchema, uiSchemaOverrides]); + + if (requireObjectSchema && jsonSchema.type !== 'object') { + return null; + } + + return ( + + ); +}; + +export default TypeForm; diff --git a/packages/ui/src/modules/types/TypeRenderer.tsx b/packages/ui/src/modules/types/TypeRenderer.tsx index 089bc81b..30370921 100644 --- a/packages/ui/src/modules/types/TypeRenderer.tsx +++ b/packages/ui/src/modules/types/TypeRenderer.tsx @@ -4,10 +4,7 @@ import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { RefreshCw } from 'lucide-react'; -import Form from '@rjsf/core'; -import validator from '@rjsf/validator-ajv8'; -import { rjsfWidgetRegistry } from './rjsfWidgetRegistry'; -import { customTemplates } from './RJSFTemplates'; +import Form from './RJSFForm'; import { generateRandomData } from './randomDataGenerator'; import { toast } from 'sonner'; @@ -35,38 +32,19 @@ export const TypeRenderer = forwardRef(({ const [showPreview, setShowPreview] = useState(false); const [previewFormData, setPreviewFormData] = useState({}); const [previewKey, setPreviewKey] = useState(0); - /* - console.log('editedType', editedType); - console.log('types', types); - console.log('jsonSchemaString', jsonSchemaString); - console.log('uiSchemaString', uiSchemaString); - */ // Generate JSON schema and UI schema when editedType changes React.useEffect(() => { if (!editedType) return; - if (['structure', 'enum', 'flags'].includes(editedType.kind)) { - const generatedSchema = generateSchemaForType(editedType.id, types); - setJsonSchemaString(JSON.stringify(generatedSchema, null, 2)); + const generatedSchema = generateSchemaForType(editedType.id, types); + setJsonSchemaString(JSON.stringify(generatedSchema, null, 2)); - // Generate UI schema recursively - const generatedUiSchema = generateUiSchemaForType(editedType.id, types); + const generatedUiSchema = generateUiSchemaForType(editedType.id, types); + // Merge with component's own UI schema (metadata) using deep merge + const finalUiSchema = deepMergeUiSchema(generatedUiSchema, editedType.meta?.uiSchema || {}); - if (editedType.kind === 'enum') { - generatedUiSchema['ui:widget'] = 'select'; - } else if (editedType.kind === 'flags') { - generatedUiSchema['ui:widget'] = 'checkboxes'; - } - - // Merge with component's own UI schema (metadata) using deep merge - const finalUiSchema = deepMergeUiSchema(generatedUiSchema, editedType.meta?.uiSchema || {}); - - setUiSchemaString(JSON.stringify(finalUiSchema, null, 2)); - } else { - setJsonSchemaString(JSON.stringify(editedType.json_schema || {}, null, 2)); - setUiSchemaString(JSON.stringify(editedType.meta?.uiSchema || {}, null, 2)); - } + setUiSchemaString(JSON.stringify(finalUiSchema, null, 2)); // Reset preview when type changes setShowPreview(false); @@ -197,9 +175,6 @@ export const TypeRenderer = forwardRef(({ schema={previewSchema} uiSchema={previewUiSchema} formData={showPreview ? previewFormData : undefined} - validator={validator} - widgets={rjsfWidgetRegistry} - templates={customTemplates} onChange={({ formData }) => showPreview && setPreviewFormData(formData)} onSubmit={({ formData }) => toast.success("Form submitted (check console)")} onError={(errors) => console.log('Form errors:', errors)} diff --git a/packages/ui/src/modules/types/TypesEditor.tsx b/packages/ui/src/modules/types/TypesEditor.tsx index 01aab2e0..e4c53e05 100644 --- a/packages/ui/src/modules/types/TypesEditor.tsx +++ b/packages/ui/src/modules/types/TypesEditor.tsx @@ -4,7 +4,7 @@ import { deepMergeUiSchema } from './schema-utils'; import { Card } from '@/components/ui/card'; import { toast } from "sonner"; import { T, translate } from '@/i18n'; -import { TypeBuilder, BuilderOutput, BuilderElement, BuilderMode, TypeBuilderRef, EnumValueEntry, FlagValueEntry } from './TypeBuilder'; +import { TypeBuilder, BuilderOutput, BuilderElement, BuilderMode, TypeBuilderRef, EnumValueEntry, FlagValueEntry } from './builder/TypeBuilder'; import { TypeRenderer, TypeRendererRef } from './TypeRenderer'; import { RefreshCw, Save, Trash2, X, Play, Languages } from "lucide-react"; import { useActions } from '@/actions/useActions'; diff --git a/packages/ui/src/modules/types/TypesList.tsx b/packages/ui/src/modules/types/TypesList.tsx index 8e2f9758..6901c8b2 100644 --- a/packages/ui/src/modules/types/TypesList.tsx +++ b/packages/ui/src/modules/types/TypesList.tsx @@ -3,7 +3,7 @@ import { TypeDefinition } from './client-types'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ScrollArea } from '@/components/ui/scroll-area'; import { TypeCategoryTree } from './TypeCategoryTree'; - +import { T } from '@/i18n' interface TypesListProps { types: TypeDefinition[]; selectedTypeId: string | null; @@ -21,7 +21,7 @@ export const TypesList: React.FC = ({ return ( - Available Types + Available Types @@ -33,11 +33,10 @@ export const TypesList: React.FC = ({ renderItem={(t) => ( - ); -} - -export function RjsfRemoveButton< - T = unknown, - S extends StrictRJSFSchema = RJSFSchema, - F extends FormContextType = Record, ->(props: IconButtonProps) { - const { className, ...rest } = props; - return ( - - - - ); -} - -export function RjsfMoveUpButton< - T = unknown, - S extends StrictRJSFSchema = RJSFSchema, - F extends FormContextType = Record, ->(props: IconButtonProps) { - const { className, ...rest } = props; - return ( - - - - ); -} - -export function RjsfMoveDownButton< - T = unknown, - S extends StrictRJSFSchema = RJSFSchema, - F extends FormContextType = Record, ->(props: IconButtonProps) { - const { className, ...rest } = props; - return ( - - - - ); -} - -export function RjsfCopyButton< - T = unknown, - S extends StrictRJSFSchema = RJSFSchema, - F extends FormContextType = Record, ->(props: IconButtonProps) { - const { className, ...rest } = props; - return ( - - - - ); -} - -/** Used for additionalProperties and other add affordances that still use the default AddButton slot. */ -export function RjsfAddIconButton< - T = unknown, - S extends StrictRJSFSchema = RJSFSchema, - F extends FormContextType = Record, ->(props: IconButtonProps) { - const { className, ...rest } = props; - return ( - - - - ); -} - -export function RjsfClearButton< - T = unknown, - S extends StrictRJSFSchema = RJSFSchema, - F extends FormContextType = Record, ->(props: IconButtonProps) { - const { className, ...rest } = props; - return ( - - - - ); -} - -/** Partial overrides merged with @rjsf/core defaults (see Form registry merge). */ -export const rjsfButtonTemplateOverrides = { - RemoveButton: RjsfRemoveButton, - MoveUpButton: RjsfMoveUpButton, - MoveDownButton: RjsfMoveDownButton, - CopyButton: RjsfCopyButton, - AddButton: RjsfAddIconButton, - ClearButton: RjsfClearButton, -}; diff --git a/packages/ui/src/modules/types/rjsfWidgetRegistry.ts b/packages/ui/src/modules/types/rjsfWidgetRegistry.ts index 87eff730..997fb05e 100644 --- a/packages/ui/src/modules/types/rjsfWidgetRegistry.ts +++ b/packages/ui/src/modules/types/rjsfWidgetRegistry.ts @@ -1,9 +1,7 @@ +// App Level Types & Widgets import type { RegistryWidgetsType } from '@rjsf/utils'; import { customWidgets } from '@/modules/types/RJSFTemplates'; -import { CategoryPickerWidget } from '@/modules/types/categoryPickerWidget'; -import { UserPickerWidget } from '@/modules/types/userPickerWidget'; -import { GroupPickerWidget } from '@/modules/types/groupPickerWidget'; -import { FilePickerWidget, PagePickerWidget, PostPickerWidget } from '@/modules/types/appPickerWidgets'; +import { CategoryPickerWidget, FilePickerWidget, GroupPickerWidget, PagePickerWidget, PostPickerWidget, UserPickerWidget } from '@/modules/types/AppTypeWidgets'; /** * Full RJSF widget registry for app forms. Re-merges picker widgets explicitly so fields like `ui:widget: categoryPicker` diff --git a/packages/ui/src/modules/types/schema-utils.ts b/packages/ui/src/modules/types/schema-utils.ts index e427dd0c..019d54d1 100644 --- a/packages/ui/src/modules/types/schema-utils.ts +++ b/packages/ui/src/modules/types/schema-utils.ts @@ -114,7 +114,7 @@ export const generateSchemaForType = (typeId: string, types: TypeDefinition[], v } // Fallback for other kinds (alias, etc -> resolve to their target if possible, currently alias is just string for now) - return { type: 'string' }; + return type.json_schema || { type: 'string' }; }; // Recursive function to generate UI schema for a type @@ -122,7 +122,17 @@ export const generateUiSchemaForType = (typeId: string, types: TypeDefinition[], if (visited.has(typeId)) return {}; const type = types.find(t => t.id === typeId); - if (!type || type.kind !== 'structure' || !type.structure_fields) { + if (!type) return {}; + + if (type.kind === 'enum') { + return { 'ui:widget': 'select' }; + } + + if (type.kind === 'flags') { + return { 'ui:widget': 'checkboxes' }; + } + + if (type.kind !== 'structure' || !type.structure_fields) { return {}; } diff --git a/packages/ui/src/modules/types/userPickerWidget.tsx b/packages/ui/src/modules/types/userPickerWidget.tsx deleted file mode 100644 index 3bd47a3e..00000000 --- a/packages/ui/src/modules/types/userPickerWidget.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import type { WidgetProps } from '@rjsf/utils'; -import { UserPicker } from '@/components/admin/UserPicker'; - -/** - * RJSF widget wrapping UserPicker. Stores selected user id (string). - */ -export const UserPickerWidget = (props: WidgetProps) => { - const { id, value, disabled, readonly, onChange, onBlur, onFocus } = props; - const v = value == null ? '' : String(value); - - if (readonly) { - return ( -
onBlur(id, value)} - onFocus={() => onFocus(id, value)} - > - {v || '—'} -
- ); - } - - return ( -
onBlur(id, value)} - onFocus={() => onFocus(id, value)} - > - onChange(userId || undefined)} - /> -
- ); -};