;
- externalId?: boolean;
- trackHistory?: boolean;
- track_history?: boolean;
- indexed?: boolean;
- reference_to?: string;
- referenceTo?: string;
- formula?: string;
-}
-
-/**
- * Convert a metadata object definition (from the API/spec) to the ObjectDefinition
- * type used by the ObjectManager component.
- */
-function toObjectDefinition(obj: MetadataObject, index: number): ObjectDefinition {
- const fields = Array.isArray(obj.fields) ? obj.fields : Object.values(obj.fields || {});
- return {
- id: obj.name || `obj_${index}`,
- name: obj.name || '',
- label: typeof obj.label === 'object' ? obj.label.defaultValue || obj.label.key || '' : (obj.label || obj.name || ''),
- pluralLabel: obj.pluralLabel || obj.plural_label || undefined,
- description: typeof obj.description === 'object' ? obj.description.defaultValue : (obj.description || undefined),
- icon: obj.icon || undefined,
- group: obj.name?.startsWith('sys_') ? 'System Objects' : 'Custom Objects',
- sortOrder: index,
- isSystem: obj.name?.startsWith('sys_') || false,
- enabled: obj.enabled !== false,
- fieldCount: fields.length,
- relationships: Array.isArray(obj.relationships)
- ? obj.relationships.map((r: any) => ({
- relatedObject: r.object || r.relatedObject || '',
- type: r.type || 'one-to-many',
- label: r.label || r.name || undefined,
- foreignKey: r.foreign_key || r.foreignKey || undefined,
- }))
- : undefined,
- };
-}
-
-/**
- * Convert a metadata field definition to the DesignerFieldDefinition
- * type used by the FieldDesigner component.
- */
-function toFieldDefinition(field: MetadataField, index: number): DesignerFieldDefinition {
- return {
- id: field.name || `fld_${index}`,
- name: field.name || '',
- label: typeof field.label === 'object' ? field.label.defaultValue || field.label.key || '' : (field.label || field.name || ''),
- type: (field.type || 'text') as DesignerFieldType,
- group: field.group || undefined,
- sortOrder: index,
- description: field.description || field.help || undefined,
- required: field.required || false,
- unique: field.unique || false,
- readonly: field.readonly || false,
- hidden: field.hidden || false,
- defaultValue: field.defaultValue || field.default_value || undefined,
- placeholder: field.placeholder || undefined,
- options: Array.isArray(field.options)
- ? field.options.map((opt) =>
- typeof opt === 'string'
- ? { label: opt, value: opt }
- : { label: opt.label || opt.value, value: opt.value, color: opt.color }
- )
- : undefined,
- isSystem: field.readonly === true && (field.name === 'id' || field.name === 'createdAt' || field.name === 'updatedAt'),
- externalId: field.externalId || false,
- trackHistory: field.trackHistory || field.track_history || false,
- indexed: field.indexed || false,
- referenceTo: field.reference_to || field.referenceTo || undefined,
- formula: field.formula || undefined,
- };
-}
+import { toObjectDefinition, toFieldDefinition, type MetadataObject } from '../../utils/metadataConverters';
// ============================================================================
// Object Detail View
@@ -265,24 +168,111 @@ function ObjectDetailView({ object, metadataObject, onBack, metadataService, onR
)}
- {/* Relationships */}
- {object.relationships && object.relationships.length > 0 && (
-
-
-
- Relationships
-
-
- {object.relationships.map((rel, i) => (
-
- {rel.label || rel.relatedObject} ({rel.type})
+
+
+ {/* Relationships Section */}
+
+
+
+ Relationships
+
+ {object.relationships && object.relationships.length > 0 ? (
+
+ {object.relationships.map((rel, i) => (
+
+
+ {rel.type}
- ))}
-
+
+ {rel.label || rel.relatedObject}
+ {rel.label && rel.label !== rel.relatedObject && (
+ → {rel.relatedObject}
+ )}
+ {rel.foreignKey && (
+ (FK: {rel.foreignKey})
+ )}
+
+
+ ))}
+ ) : (
+
No relationships defined for this object.
)}
+ {/* Keys Section */}
+
+
+
+ Keys
+
+ {(() => {
+ const keyFields = displayFields.filter(
+ (f) => f.unique || f.name === 'id' || f.externalId
+ );
+ if (keyFields.length > 0) {
+ return (
+
+ {keyFields.map((kf) => (
+
+
+ {kf.name === 'id' ? 'Primary Key' : kf.externalId ? 'External ID' : 'Unique'}
+
+
+ {kf.label || kf.name}
+ ({kf.type})
+
+
+ ))}
+
+ );
+ }
+ return (
+
No unique keys or primary keys found.
+ );
+ })()}
+
+
+ {/* Data Experience Section */}
+
+
+
+ Data Experience
+
+
+
+
+
Forms
+
Design forms for data entry
+
+
+
+
Views
+
Configure list and detail views
+
+
+
+
Dashboards
+
Build visual dashboards
+
+
+
+
+ {/* Inline Data Preview (placeholder) */}
+
+
+
+ Data Preview
+
+
+
+
Sample Data
+
+ Live data preview for “{object.label}” will be available here
+
+
+
+
{/* Field Management Section */}
{saving && (
@@ -291,6 +281,13 @@ function ObjectDetailView({ object, metadataObject, onBack, metadataService, onR
Saving field changes…
)}
+ {/* System field hint */}
+ {displayFields.some((f) => f.isSystem) && (
+
+
+ System fields (e.g. id, createdAt, updatedAt) are read-only and cannot be edited or deleted.
+
+ )}
> = {
- 'layout-grid': LayoutGrid,
- 'database': Database,
- 'layout-dashboard': LayoutDashboard,
- 'file-text': FileText,
- 'bar-chart-3': BarChart3,
-};
-
-function resolveIcon(iconName: string): React.ComponentType<{ className?: string }> {
- return ICON_MAP[iconName] ?? Database;
-}
-
export function SystemHubPage() {
const navigate = useNavigate();
const { appName } = useParams();
@@ -122,7 +103,7 @@ export function SystemHubPage() {
const href = cfg.hasCustomPage && cfg.customRoute
? `${basePath}${cfg.customRoute}`
: `${basePath}/system/metadata/${cfg.type}`;
- const Icon = resolveIcon(cfg.icon);
+ const Icon = getIcon(cfg.icon);
// Resolve count from metadata context or data source counts
let count: number | null = null;
diff --git a/apps/console/src/utils/metadataConverters.ts b/apps/console/src/utils/metadataConverters.ts
new file mode 100644
index 000000000..8b7c29eef
--- /dev/null
+++ b/apps/console/src/utils/metadataConverters.ts
@@ -0,0 +1,131 @@
+/**
+ * Metadata Converters
+ *
+ * Shared conversion functions for transforming raw metadata API objects
+ * (from the ObjectStack spec) to the UI types used by ObjectManager and
+ * FieldDesigner components.
+ *
+ * Extracted from ObjectManagerPage to enable reuse across pages.
+ *
+ * @module utils/metadataConverters
+ */
+
+import type { ObjectDefinition, DesignerFieldDefinition, DesignerFieldType } from '@object-ui/types';
+
+// ---------------------------------------------------------------------------
+// Raw metadata shapes (from the ObjectStack API)
+// ---------------------------------------------------------------------------
+
+/** Loose shape of a metadata object definition from the ObjectStack API. */
+export interface MetadataObject {
+ name?: string;
+ label?: string | { defaultValue?: string; key?: string };
+ pluralLabel?: string;
+ plural_label?: string;
+ description?: string | { defaultValue?: string };
+ icon?: string;
+ enabled?: boolean;
+ fields?: MetadataField[] | Record;
+ relationships?: Array<{
+ object?: string;
+ relatedObject?: string;
+ type?: string;
+ label?: string;
+ name?: string;
+ foreign_key?: string;
+ foreignKey?: string;
+ }>;
+}
+
+/** Loose shape of a metadata field definition from the ObjectStack API. */
+export interface MetadataField {
+ name?: string;
+ label?: string | { defaultValue?: string; key?: string };
+ type?: string;
+ group?: string;
+ description?: string;
+ help?: string;
+ required?: boolean;
+ unique?: boolean;
+ readonly?: boolean;
+ hidden?: boolean;
+ defaultValue?: string;
+ default_value?: string;
+ placeholder?: string;
+ options?: Array;
+ externalId?: boolean;
+ trackHistory?: boolean;
+ track_history?: boolean;
+ indexed?: boolean;
+ reference_to?: string;
+ referenceTo?: string;
+ formula?: string;
+}
+
+// ---------------------------------------------------------------------------
+// Converters
+// ---------------------------------------------------------------------------
+
+/**
+ * Convert a metadata object definition (from the API/spec) to the ObjectDefinition
+ * type used by the ObjectManager component.
+ */
+export function toObjectDefinition(obj: MetadataObject, index: number): ObjectDefinition {
+ const fields = Array.isArray(obj.fields) ? obj.fields : Object.values(obj.fields || {});
+ return {
+ id: obj.name || `obj_${index}`,
+ name: obj.name || '',
+ label: typeof obj.label === 'object' ? obj.label.defaultValue || obj.label.key || '' : (obj.label || obj.name || ''),
+ pluralLabel: obj.pluralLabel || obj.plural_label || undefined,
+ description: typeof obj.description === 'object' ? obj.description.defaultValue : (obj.description || undefined),
+ icon: obj.icon || undefined,
+ group: obj.name?.startsWith('sys_') ? 'System Objects' : 'Custom Objects',
+ sortOrder: index,
+ isSystem: obj.name?.startsWith('sys_') || false,
+ enabled: obj.enabled !== false,
+ fieldCount: fields.length,
+ relationships: Array.isArray(obj.relationships)
+ ? obj.relationships.map((r) => ({
+ relatedObject: r.object || r.relatedObject || '',
+ type: r.type || 'one-to-many',
+ label: r.label || r.name || undefined,
+ foreignKey: r.foreign_key || r.foreignKey || undefined,
+ }))
+ : undefined,
+ };
+}
+
+/**
+ * Convert a metadata field definition to the DesignerFieldDefinition
+ * type used by the FieldDesigner component.
+ */
+export function toFieldDefinition(field: MetadataField, index: number): DesignerFieldDefinition {
+ return {
+ id: field.name || `fld_${index}`,
+ name: field.name || '',
+ label: typeof field.label === 'object' ? field.label.defaultValue || field.label.key || '' : (field.label || field.name || ''),
+ type: (field.type || 'text') as DesignerFieldType,
+ group: field.group || undefined,
+ sortOrder: index,
+ description: field.description || field.help || undefined,
+ required: field.required || false,
+ unique: field.unique || false,
+ readonly: field.readonly || false,
+ hidden: field.hidden || false,
+ defaultValue: field.defaultValue || field.default_value || undefined,
+ placeholder: field.placeholder || undefined,
+ options: Array.isArray(field.options)
+ ? field.options.map((opt) =>
+ typeof opt === 'string'
+ ? { label: opt, value: opt }
+ : { label: opt.label || opt.value, value: opt.value, color: opt.color }
+ )
+ : undefined,
+ isSystem: field.readonly === true && (field.name === 'id' || field.name === 'createdAt' || field.name === 'updatedAt'),
+ externalId: field.externalId || false,
+ trackHistory: field.trackHistory || field.track_history || false,
+ indexed: field.indexed || false,
+ referenceTo: field.reference_to || field.referenceTo || undefined,
+ formula: field.formula || undefined,
+ };
+}