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 {