diff --git a/apps/console/src/__tests__/MetadataDetailPage.test.tsx b/apps/console/src/__tests__/MetadataDetailPage.test.tsx
index b5372ffc..5533f2f8 100644
--- a/apps/console/src/__tests__/MetadataDetailPage.test.tsx
+++ b/apps/console/src/__tests__/MetadataDetailPage.test.tsx
@@ -77,6 +77,7 @@ beforeEach(() => {
);
+ ComponentRegistry.register('object-detail-tabs', mockWidget('object-detail-tabs'));
ComponentRegistry.register('object-properties', mockWidget('object-properties'));
ComponentRegistry.register('object-relationships', mockWidget('object-relationships'));
ComponentRegistry.register('object-keys', mockWidget('object-keys'));
@@ -209,7 +210,7 @@ describe('MetadataDetailPage', () => {
expect(screen.getByTestId('schema-detail-content')).toBeInTheDocument();
});
- it('should render all object detail widget sections', () => {
+ it('should render object detail tabs widget', () => {
mockGetItems.mockResolvedValue([
{ name: 'account', label: 'Accounts', description: 'Customer accounts' },
]);
@@ -223,13 +224,8 @@ describe('MetadataDetailPage', () => {
,
);
- // All widget sections should be rendered via SchemaRenderer
- expect(screen.getByTestId('mock-object-properties')).toBeInTheDocument();
- expect(screen.getByTestId('mock-object-relationships')).toBeInTheDocument();
- expect(screen.getByTestId('mock-object-keys')).toBeInTheDocument();
- expect(screen.getByTestId('mock-object-data-experience')).toBeInTheDocument();
- expect(screen.getByTestId('mock-object-data-preview')).toBeInTheDocument();
- expect(screen.getByTestId('mock-object-field-designer')).toBeInTheDocument();
+ // The tabbed widget should be rendered
+ expect(screen.getByTestId('mock-object-detail-tabs')).toBeInTheDocument();
});
it('should navigate back to object list route when back button is clicked', () => {
diff --git a/apps/console/src/components/schema/ObjectDetailTabsWidget.tsx b/apps/console/src/components/schema/ObjectDetailTabsWidget.tsx
new file mode 100644
index 00000000..56703fe4
--- /dev/null
+++ b/apps/console/src/components/schema/ObjectDetailTabsWidget.tsx
@@ -0,0 +1,130 @@
+/**
+ * Object Detail Tabs Widget
+ *
+ * Self-contained, schema-driven tabbed widget for the object detail page.
+ * Organizes object configuration into logical tabs similar to Power Apps:
+ * - Details: Object properties and basic information
+ * - Fields: Field designer for managing object fields
+ * - Relationships: Relationships and unique keys
+ * - Data: Data preview and experience placeholders
+ *
+ * Schema: { type: 'object-detail-tabs', objectName: 'account' }
+ *
+ * @module components/schema/ObjectDetailTabsWidget
+ */
+
+import { useState } from 'react';
+import { Tabs, TabsList, TabsTrigger, TabsContent } from '@object-ui/components';
+import { Settings2, Columns, Link2, Table } from 'lucide-react';
+import type { SchemaNode } from '@object-ui/core';
+import { SchemaRenderer } from '@object-ui/react';
+
+/** Schema props for object detail tabs widget. */
+interface ObjectDetailTabsSchema extends SchemaNode {
+ objectName: string;
+}
+
+export function ObjectDetailTabsWidget({ schema }: { schema: ObjectDetailTabsSchema }) {
+ const objectName = schema.objectName;
+ const [activeTab, setActiveTab] = useState('details');
+
+ // Create schema objects for each widget
+ const detailsSchema: SchemaNode & { objectName: string } = {
+ type: 'object-properties',
+ id: `${objectName}-properties`,
+ objectName,
+ };
+
+ const fieldsSchema: SchemaNode & { objectName: string } = {
+ type: 'object-field-designer',
+ id: `${objectName}-field-designer`,
+ objectName,
+ };
+
+ const relationshipsSchema: SchemaNode & { objectName: string } = {
+ type: 'object-relationships',
+ id: `${objectName}-relationships`,
+ objectName,
+ };
+
+ const keysSchema: SchemaNode & { objectName: string } = {
+ type: 'object-keys',
+ id: `${objectName}-keys`,
+ objectName,
+ };
+
+ const dataExperienceSchema: SchemaNode & { objectName: string } = {
+ type: 'object-data-experience',
+ id: `${objectName}-data-experience`,
+ objectName,
+ };
+
+ const dataPreviewSchema: SchemaNode & { objectName: string } = {
+ type: 'object-data-preview',
+ id: `${objectName}-data-preview`,
+ objectName,
+ };
+
+ return (
+
+
+
+
+
+ Details
+
+
+
+ Fields
+
+
+
+ Relationships
+
+
+
+ Data
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/console/src/components/schema/objectDetailWidgets.tsx b/apps/console/src/components/schema/objectDetailWidgets.tsx
index 51943ed9..88471085 100644
--- a/apps/console/src/components/schema/objectDetailWidgets.tsx
+++ b/apps/console/src/components/schema/objectDetailWidgets.tsx
@@ -14,7 +14,6 @@
import { useMemo } from 'react';
import { Badge } from '@object-ui/components';
import {
- Settings2,
Link2,
KeyRound,
LayoutList,
@@ -75,52 +74,67 @@ export function ObjectPropertiesWidget({ schema }: { schema: ObjectWidgetSchema
if (!object) return null;
return (
-
-
-
- Object Properties
-
-
-
-
API Name
-
{object.name}
-
-
-
Label
-
{object.label}
-
- {object.pluralLabel && (
-
-
Plural Label
-
{object.pluralLabel}
+
+ {/* Basic Information Section */}
+
+
+ Basic Information
+
+
+
+
- Display Name
+ - {object.label}
- )}
- {object.group && (
-
-
Group
-
{object.group}
+
+
- API Name
+ - {object.name}
- )}
-
-
Status
-
-
- {object.enabled !== false ? 'Enabled' : 'Disabled'}
-
-
-
-
- Fields
- {object.fieldCount ?? fields.length}
-
- {object.isSystem && (
-
-
Type
-
- System Object
-
+ {object.pluralLabel && (
+
+
- Plural Label
+ - {object.pluralLabel}
+
+ )}
+ {object.group && (
+
+
- Group
+ - {object.group}
+
+ )}
+
+
+
+ {/* Configuration Section */}
+
+
+ Configuration
+
+
+
+
- Status
+ -
+
+ {object.enabled !== false ? 'Enabled' : 'Disabled'}
+
+
- )}
+
+
- Field Count
+ -
+ {(object.fieldCount ?? fields.length) === 1
+ ? '1 field'
+ : `${object.fieldCount ?? fields.length} fields`}
+
+
+ {object.isSystem && (
+
+
- Type
+ -
+ System Object
+
+
+ )}
+
);
@@ -137,34 +151,47 @@ export function ObjectRelationshipsWidget({ schema }: { schema: ObjectWidgetSche
if (!object) return null;
+ const hasRelationships = object.relationships && object.relationships.length > 0;
+
return (
-
-
-
- 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})
- )}
+
+
+
+ Relationships
+
+ {hasRelationships ? (
+
+ {object.relationships.map((rel, i) => (
+
+
+ {rel.type}
+
+
+
{rel.label || rel.relatedObject}
+ {rel.label && rel.label !== rel.relatedObject && (
+
+ Related Object: {rel.relatedObject}
+
+ )}
+ {rel.foreignKey && (
+
+ Foreign Key: {rel.foreignKey}
+
+ )}
+
-
- ))}
-
- ) : (
-
No relationships defined for this object.
- )}
+ ))}
+
+ ) : (
+
+
+
No relationships defined for this object.
+
+ )}
+
);
}
@@ -184,28 +211,40 @@ export function ObjectKeysWidget({ schema }: { schema: ObjectWidgetSchema }) {
);
return (
-
-
-
- Keys
-
- {keyFields.length > 0 ? (
-
- {keyFields.map((kf) => (
-
-
- {kf.name === 'id' ? 'Primary Key' : kf.externalId ? 'External ID' : 'Unique'}
-
-
-
{kf.label || kf.name}
-
({kf.type})
+
+
+
+ Unique Keys
+
+ {keyFields.length > 0 ? (
+
+ {keyFields.map((kf) => (
+
+
+ {kf.name === 'id' ? 'Primary Key' : kf.externalId ? 'External ID' : 'Unique'}
+
+
+
{kf.label || kf.name}
+
+ Type: {kf.type}
+
+
-
- ))}
-
- ) : (
-
No unique keys or primary keys found.
- )}
+ ))}
+
+ ) : (
+
+
+
No unique keys or primary keys found.
+
+ )}
+
);
}
@@ -217,26 +256,30 @@ export function ObjectKeysWidget({ schema }: { schema: ObjectWidgetSchema }) {
export function ObjectDataExperienceWidget(_props: { schema: ObjectWidgetSchema }) {
return (
-
-
-
- Data Experience
-
-
-
-
-
Forms
-
Design forms for data entry
-
-
-
-
Views
-
Configure list and detail views
-
-
-
-
Dashboards
-
Build visual dashboards
+
+
+
+ Data Experience
+
+
+ Configure how users interact with data in this object
+
+
+
+
+
Forms
+
Design forms for data entry
+
+
+
+
Views
+
Configure list and detail views
+
+
+
+
Dashboards
+
Build visual dashboards
+
@@ -253,17 +296,21 @@ export function ObjectDataPreviewWidget({ schema }: { schema: ObjectWidgetSchema
const { object } = useObjectData(objectName);
return (
-
-
-
- Data Preview
-
-
-
-
Sample Data
-
- Live data preview for “{object?.label || objectName}” will be available here
+
+
+
+ Data Preview
+
+
+ View sample records from this object
+
+
+
Sample Data
+
+ Live data preview for “{object?.label || objectName}” will be available here
+
+
);
diff --git a/apps/console/src/components/schema/registerObjectDetailWidgets.ts b/apps/console/src/components/schema/registerObjectDetailWidgets.ts
index 46b93df3..6c66ad35 100644
--- a/apps/console/src/components/schema/registerObjectDetailWidgets.ts
+++ b/apps/console/src/components/schema/registerObjectDetailWidgets.ts
@@ -6,6 +6,7 @@
* so that SchemaRenderer can resolve these types when rendering PageSchema.
*
* Widget types registered:
+ * - `object-detail-tabs` — Tabbed layout for object detail page
* - `object-properties` — Object property card
* - `object-relationships` — Relationships card
* - `object-keys` — Keys card
@@ -25,12 +26,19 @@ import {
ObjectDataPreviewWidget,
} from './objectDetailWidgets';
import { ObjectFieldDesignerWidget } from './ObjectFieldDesignerWidget';
+import { ObjectDetailTabsWidget } from './ObjectDetailTabsWidget';
const widgetMeta = {
namespace: 'console',
category: 'system',
};
+ComponentRegistry.register('object-detail-tabs', ObjectDetailTabsWidget, {
+ ...widgetMeta,
+ label: 'Object Detail Tabs',
+ icon: 'LayoutDashboard',
+});
+
ComponentRegistry.register('object-properties', ObjectPropertiesWidget, {
...widgetMeta,
label: 'Object Properties',
diff --git a/apps/console/src/schemas/objectDetailPageSchema.ts b/apps/console/src/schemas/objectDetailPageSchema.ts
index c79f4368..4a095d79 100644
--- a/apps/console/src/schemas/objectDetailPageSchema.ts
+++ b/apps/console/src/schemas/objectDetailPageSchema.ts
@@ -1,10 +1,10 @@
/**
* Object Detail Page Schema Factory
*
- * Generates a PageSchema for the object detail page. The schema uses custom
- * widget types (registered in ComponentRegistry via registerObjectDetailWidgets)
- * that are fully self-contained — each widget resolves its data from React
- * context rather than requiring prop drilling.
+ * Generates a PageSchema for the object detail page with a tabbed layout.
+ * The schema uses custom widget types (registered in ComponentRegistry via
+ * registerObjectDetailWidgets) that are fully self-contained — each widget
+ * resolves its data from React context rather than requiring prop drilling.
*
* Usage:
* const schema = buildObjectDetailPageSchema('account', metadataItem);
@@ -21,7 +21,13 @@ interface ObjectWidgetNode extends BaseSchema {
}
/**
- * Build a PageSchema for an object detail page.
+ * Build a PageSchema for an object detail page with tabbed navigation.
+ *
+ * Tabs:
+ * - Details: Object properties and basic information
+ * - Fields: Field designer for managing object fields
+ * - Relationships: Relationships and unique keys
+ * - Data: Data preview and experience placeholders
*
* @param objectName - The API name of the object (e.g. 'account')
* @param item - The raw metadata item (optional, used for title/description)
@@ -35,12 +41,7 @@ export function buildObjectDetailPageSchema(
const description = (item?.description as string) || objectName;
const widgets: ObjectWidgetNode[] = [
- { type: 'object-properties', id: `${objectName}-properties`, objectName },
- { type: 'object-relationships', id: `${objectName}-relationships`, objectName },
- { type: 'object-keys', id: `${objectName}-keys`, objectName },
- { type: 'object-data-experience', id: `${objectName}-data-experience`, objectName },
- { type: 'object-data-preview', id: `${objectName}-data-preview`, objectName },
- { type: 'object-field-designer', id: `${objectName}-field-designer`, objectName },
+ { type: 'object-detail-tabs', id: `${objectName}-tabs`, objectName },
];
return {