diff --git a/examples/object-view-demo/README.md b/examples/object-view-demo/README.md
new file mode 100644
index 0000000..9852301
--- /dev/null
+++ b/examples/object-view-demo/README.md
@@ -0,0 +1,88 @@
+# ObjectView Demo
+
+A comprehensive demonstration of the `ObjectView` component, which integrates `ObjectTable` and `ObjectForm` into a complete, ready-to-use CRUD interface.
+
+## Features
+
+- **Integrated Table and Form**: Seamless combination of list view and create/edit forms
+- **Multiple Layout Modes**: Switch between drawer and modal layouts for form display
+- **Search Functionality**: Search across all records
+- **Filter Builder**: Placeholder for advanced filtering capabilities
+- **CRUD Operations**: Complete Create, Read, Update, Delete functionality
+- **Bulk Actions**: Select multiple rows and perform bulk delete
+- **Auto-refresh**: Table automatically refreshes after form submission
+- **Mock Data Source**: Demonstrates with in-memory mock data
+
+## Running the Demo
+
+```bash
+# From the root of the repository
+pnpm install
+pnpm --filter @examples/object-view-demo dev
+```
+
+Then open your browser to `http://localhost:5173`
+
+## Usage
+
+The demo showcases:
+
+1. **Drawer Mode (Default)**: Forms slide in from the right side
+2. **Modal Mode**: Forms appear in a centered modal dialog
+
+### Actions
+
+- **Create**: Click the "Create" button in the toolbar
+- **View**: Click on any row in the table
+- **Edit**: Click the "Edit" button in the actions column
+- **Delete**: Click the "Delete" button (with confirmation)
+- **Bulk Delete**: Select multiple rows using checkboxes and click "Delete Selected"
+- **Search**: Type in the search box to filter records
+- **Filters**: Click the "Filters" button to toggle the filter panel (placeholder)
+- **Refresh**: Click the refresh button to reload the table
+
+## Component Overview
+
+The `ObjectView` component combines:
+
+- **ObjectTable**: Auto-generated table with schema-based columns
+- **ObjectForm**: Auto-generated form with schema-based fields
+- **Drawer/Modal**: Configurable overlay components for form display
+- **Search Bar**: Built-in search functionality
+- **Filter Panel**: Placeholder for future filter builder integration
+- **Toolbar**: Actions bar with create, refresh, and filter buttons
+
+## Schema Configuration
+
+```typescript
+const schema: ObjectViewSchema = {
+ type: 'object-view',
+ objectName: 'users',
+ layout: 'drawer', // or 'modal'
+ showSearch: true,
+ showFilters: true,
+ showCreate: true,
+ operations: {
+ create: true,
+ read: true,
+ update: true,
+ delete: true,
+ },
+ table: {
+ fields: ['name', 'email', 'status', 'role'],
+ selectable: 'multiple',
+ },
+ form: {
+ fields: ['name', 'email', 'status', 'role'],
+ layout: 'vertical',
+ },
+};
+```
+
+## Next Steps
+
+- Integrate real ObjectQL backend
+- Implement advanced filtering with FilterBuilder
+- Add pagination controls
+- Add export/import functionality
+- Add custom actions and bulk operations
diff --git a/examples/object-view-demo/index.html b/examples/object-view-demo/index.html
new file mode 100644
index 0000000..388cf01
--- /dev/null
+++ b/examples/object-view-demo/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ ObjectView Demo - ObjectUI
+
+
+
+
+
+
diff --git a/examples/object-view-demo/package.json b/examples/object-view-demo/package.json
new file mode 100644
index 0000000..2308cc5
--- /dev/null
+++ b/examples/object-view-demo/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@examples/object-view-demo",
+ "private": true,
+ "license": "MIT",
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@object-ui/types": "workspace:*",
+ "@object-ui/core": "workspace:*",
+ "@object-ui/react": "workspace:*",
+ "@object-ui/components": "workspace:*",
+ "@object-ui/plugin-object": "workspace:*",
+ "@object-ui/data-objectql": "workspace:*",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "lucide-react": "^0.562.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/node": "^25.0.9",
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^5.1.2",
+ "autoprefixer": "^10.4.23",
+ "eslint": "^9.39.2",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^3.4.19",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "vite": "^7.3.1"
+ }
+}
diff --git a/examples/object-view-demo/postcss.config.js b/examples/object-view-demo/postcss.config.js
new file mode 100644
index 0000000..cf1275e
--- /dev/null
+++ b/examples/object-view-demo/postcss.config.js
@@ -0,0 +1,14 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/examples/object-view-demo/src/App.tsx b/examples/object-view-demo/src/App.tsx
new file mode 100644
index 0000000..f244434
--- /dev/null
+++ b/examples/object-view-demo/src/App.tsx
@@ -0,0 +1,253 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { useState } from 'react';
+import { ObjectView } from '@object-ui/plugin-object';
+import type { ObjectViewSchema } from '@object-ui/types';
+import type { ObjectQLDataSource } from '@object-ui/data-objectql';
+
+// Mock data for demonstration
+const mockUsers = [
+ { _id: '1', name: 'John Doe', email: 'john@example.com', status: 'active', role: 'Admin', createdAt: '2024-01-15' },
+ { _id: '2', name: 'Jane Smith', email: 'jane@example.com', status: 'active', role: 'User', createdAt: '2024-01-16' },
+ { _id: '3', name: 'Bob Johnson', email: 'bob@example.com', status: 'inactive', role: 'User', createdAt: '2024-01-17' },
+ { _id: '4', name: 'Alice Williams', email: 'alice@example.com', status: 'active', role: 'Editor', createdAt: '2024-01-18' },
+ { _id: '5', name: 'Charlie Brown', email: 'charlie@example.com', status: 'active', role: 'User', createdAt: '2024-01-19' },
+];
+
+// Mock ObjectQL data source
+const createMockDataSource = (): ObjectQLDataSource => {
+ let data = [...mockUsers];
+
+ return {
+ // Find all records
+ find: async (objectName: string, params?: Record) => {
+ console.log('find:', objectName, params);
+ await new Promise((resolve) => setTimeout(resolve, 300)); // Simulate network delay
+ return {
+ data: data,
+ total: data.length,
+ };
+ },
+
+ // Find one record by ID
+ findOne: async (objectName: string, id: string | number) => {
+ console.log('findOne:', objectName, id);
+ await new Promise((resolve) => setTimeout(resolve, 200));
+ return data.find((item) => item._id === id) || null;
+ },
+
+ // Create a new record
+ create: async (objectName: string, record: Record) => {
+ console.log('create:', objectName, record);
+ await new Promise((resolve) => setTimeout(resolve, 300));
+ const newRecord = {
+ _id: String(Date.now()),
+ createdAt: new Date().toISOString().split('T')[0],
+ ...record,
+ };
+ data.push(newRecord as typeof mockUsers[0]);
+ return newRecord;
+ },
+
+ // Update an existing record
+ update: async (objectName: string, id: string | number, record: Record) => {
+ console.log('update:', objectName, id, record);
+ await new Promise((resolve) => setTimeout(resolve, 300));
+ const index = data.findIndex((item) => item._id === id);
+ if (index !== -1) {
+ data[index] = { ...data[index], ...record };
+ return data[index];
+ }
+ throw new Error('Record not found');
+ },
+
+ // Delete a record
+ delete: async (objectName: string, id: string | number) => {
+ console.log('delete:', objectName, id);
+ await new Promise((resolve) => setTimeout(resolve, 300));
+ const index = data.findIndex((item) => item._id === id);
+ if (index !== -1) {
+ data.splice(index, 1);
+ return true;
+ }
+ return false;
+ },
+
+ // Bulk operations
+ bulk: async (objectName: string, operation: string, records: Record[]) => {
+ console.log('bulk:', objectName, operation, records);
+ await new Promise((resolve) => setTimeout(resolve, 400));
+ if (operation === 'delete') {
+ const ids = records.map((r) => r._id || r.id);
+ data = data.filter((item) => !ids.includes(item._id));
+ return records;
+ }
+ return [];
+ },
+
+ // Get object schema
+ getObjectSchema: async (objectName: string) => {
+ console.log('getObjectSchema:', objectName);
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ return {
+ name: objectName,
+ label: 'Users',
+ fields: {
+ name: {
+ type: 'text',
+ label: 'Name',
+ required: true,
+ placeholder: 'Enter full name',
+ },
+ email: {
+ type: 'email',
+ label: 'Email',
+ required: true,
+ placeholder: 'user@example.com',
+ },
+ status: {
+ type: 'select',
+ label: 'Status',
+ required: true,
+ options: [
+ { value: 'active', label: 'Active' },
+ { value: 'inactive', label: 'Inactive' },
+ ],
+ },
+ role: {
+ type: 'select',
+ label: 'Role',
+ required: true,
+ options: [
+ { value: 'Admin', label: 'Admin' },
+ { value: 'Editor', label: 'Editor' },
+ { value: 'User', label: 'User' },
+ ],
+ },
+ createdAt: {
+ type: 'date',
+ label: 'Created At',
+ readonly: true,
+ },
+ },
+ };
+ },
+ } as ObjectQLDataSource;
+};
+
+function App() {
+ const [layout, setLayout] = useState<'drawer' | 'modal' | 'page'>('drawer');
+ const [dataSource] = useState(createMockDataSource());
+
+ const schema: ObjectViewSchema = {
+ type: 'object-view',
+ objectName: 'users',
+ title: 'User Management',
+ description: 'Manage users in your system with search, filters, and CRUD operations.',
+ layout: layout,
+ showSearch: true,
+ showFilters: true,
+ showCreate: true,
+ showRefresh: true,
+ operations: {
+ create: true,
+ read: true,
+ update: true,
+ delete: true,
+ },
+ table: {
+ fields: ['name', 'email', 'status', 'role', 'createdAt'],
+ pageSize: 10,
+ selectable: 'multiple',
+ defaultSort: {
+ field: 'name',
+ order: 'asc',
+ },
+ },
+ form: {
+ fields: ['name', 'email', 'status', 'role'],
+ layout: 'vertical',
+ columns: 1,
+ },
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
ObjectView Demo
+
+ Complete CRUD interface with integrated table and form
+
+
+
+ {/* Layout Selector */}
+
+
+
+
+
+
+
+
+ {/* Main Content */}
+
+
+ {/* Footer */}
+
+
+
+
Features Demonstrated:
+
+ - Integrated ObjectTable with automatic column generation
+ - ObjectForm with drawer/modal layouts for create/edit operations
+ - Search functionality across records
+ - Filter builder placeholder for advanced filtering
+ - CRUD operations (Create, Read, Update, Delete)
+ - Bulk delete with row selection
+ - Auto-refresh after form submission
+ - Responsive design with Tailwind CSS
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/examples/object-view-demo/src/index.css b/examples/object-view-demo/src/index.css
new file mode 100644
index 0000000..17df0e7
--- /dev/null
+++ b/examples/object-view-demo/src/index.css
@@ -0,0 +1,17 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/examples/object-view-demo/src/main.tsx b/examples/object-view-demo/src/main.tsx
new file mode 100644
index 0000000..89f11d4
--- /dev/null
+++ b/examples/object-view-demo/src/main.tsx
@@ -0,0 +1,22 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import App from './App';
+import '@object-ui/components/index.css';
+import './index.css';
+
+const root = document.getElementById('root');
+if (root) {
+ createRoot(root).render(
+
+
+
+ );
+}
diff --git a/examples/object-view-demo/tailwind.config.js b/examples/object-view-demo/tailwind.config.js
new file mode 100644
index 0000000..30c71d9
--- /dev/null
+++ b/examples/object-view-demo/tailwind.config.js
@@ -0,0 +1,24 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import componentConfig from '../../packages/components/tailwind.config.js';
+
+/** @type {import('tailwindcss').Config} */
+export default {
+ presets: [componentConfig],
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ "../../packages/designer/src/**/*.{js,ts,jsx,tsx}",
+ "../../packages/components/src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
diff --git a/examples/object-view-demo/tsconfig.json b/examples/object-view-demo/tsconfig.json
new file mode 100644
index 0000000..4723c2d
--- /dev/null
+++ b/examples/object-view-demo/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/examples/object-view-demo/vite.config.ts b/examples/object-view-demo/vite.config.ts
new file mode 100644
index 0000000..4a41fca
--- /dev/null
+++ b/examples/object-view-demo/vite.config.ts
@@ -0,0 +1,26 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import path from 'path';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': '/src',
+ '@object-ui/core': path.resolve(__dirname, '../../packages/core/src'),
+ '@object-ui/types': path.resolve(__dirname, '../../packages/types/src'),
+ '@object-ui/components': path.resolve(__dirname, '../../packages/components/src'),
+ '@object-ui/designer': path.resolve(__dirname, '../../packages/designer/src'),
+ '@object-ui/react': path.resolve(__dirname, '../../packages/react/src'),
+ },
+ },
+});
diff --git a/packages/plugin-object/src/ObjectView.tsx b/packages/plugin-object/src/ObjectView.tsx
new file mode 100644
index 0000000..c363bc5
--- /dev/null
+++ b/packages/plugin-object/src/ObjectView.tsx
@@ -0,0 +1,410 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * ObjectView Component
+ *
+ * A complete object management interface that combines ObjectTable and ObjectForm.
+ * Provides list view with integrated search, filters, and create/edit operations.
+ */
+
+import React, { useEffect, useState, useCallback } from 'react';
+import type { ObjectViewSchema, ObjectTableSchema, ObjectFormSchema } from '@object-ui/types';
+import type { ObjectQLDataSource } from '@object-ui/data-objectql';
+import { ObjectTable } from './ObjectTable';
+import { ObjectForm } from './ObjectForm';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ Drawer,
+ DrawerContent,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerDescription,
+ Button,
+ Input,
+} from '@object-ui/components';
+import { Plus, Search, RefreshCw, Filter, X } from 'lucide-react';
+
+export interface ObjectViewProps {
+ /**
+ * The schema configuration for the view
+ */
+ schema: ObjectViewSchema;
+
+ /**
+ * ObjectQL data source
+ */
+ dataSource: ObjectQLDataSource;
+
+ /**
+ * Additional CSS class
+ */
+ className?: string;
+}
+
+type FormMode = 'create' | 'edit' | 'view';
+
+/**
+ * ObjectView Component
+ *
+ * Renders a complete object management interface with table and forms.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export const ObjectView: React.FC = ({
+ schema,
+ dataSource,
+ className,
+}) => {
+ const [objectSchema, setObjectSchema] = useState | null>(null);
+ const [isFormOpen, setIsFormOpen] = useState(false);
+ const [formMode, setFormMode] = useState('create');
+ const [selectedRecord, setSelectedRecord] = useState | null>(null);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [showFilters, setShowFilters] = useState(false);
+ const [refreshKey, setRefreshKey] = useState(0);
+
+ // Fetch object schema from ObjectQL
+ useEffect(() => {
+ const fetchObjectSchema = async () => {
+ try {
+ const schemaData = await dataSource.getObjectSchema(schema.objectName);
+ setObjectSchema(schemaData);
+ } catch (err) {
+ console.error('Failed to fetch object schema:', err);
+ }
+ };
+
+ if (schema.objectName && dataSource) {
+ fetchObjectSchema();
+ }
+ }, [schema.objectName, dataSource]);
+
+ // Determine layout mode
+ const layout = schema.layout || 'drawer';
+
+ // Determine enabled operations
+ const operations = schema.operations || schema.table?.operations || {
+ create: true,
+ read: true,
+ update: true,
+ delete: true,
+ };
+
+ // Handle create action
+ const handleCreate = useCallback(() => {
+ if (layout === 'page' && schema.onNavigate) {
+ schema.onNavigate('new', 'edit');
+ } else {
+ setFormMode('create');
+ setSelectedRecord(null);
+ setIsFormOpen(true);
+ }
+ }, [layout, schema]);
+
+ // Handle edit action
+ const handleEdit = useCallback((record: Record) => {
+ if (layout === 'page' && schema.onNavigate) {
+ const recordId = record._id || record.id;
+ schema.onNavigate(recordId as string | number, 'edit');
+ } else {
+ setFormMode('edit');
+ setSelectedRecord(record);
+ setIsFormOpen(true);
+ }
+ }, [layout, schema]);
+
+ // Handle view action
+ const handleView = useCallback((record: Record) => {
+ if (layout === 'page' && schema.onNavigate) {
+ const recordId = record._id || record.id;
+ schema.onNavigate(recordId as string | number, 'view');
+ } else {
+ setFormMode('view');
+ setSelectedRecord(record);
+ setIsFormOpen(true);
+ }
+ }, [layout, schema]);
+
+ // Handle row click
+ const handleRowClick = useCallback((record: Record) => {
+ if (operations.read !== false) {
+ handleView(record);
+ }
+ }, [operations.read, handleView]);
+
+ // Handle delete action
+ const handleDelete = useCallback((_record: Record) => {
+ // Trigger table refresh after delete
+ setRefreshKey(prev => prev + 1);
+ }, []);
+
+ // Handle bulk delete action
+ const handleBulkDelete = useCallback((_records: Record[]) => {
+ // Trigger table refresh after bulk delete
+ setRefreshKey(prev => prev + 1);
+ }, []);
+
+ // Handle form submission
+ const handleFormSuccess = useCallback(() => {
+ // Close the form
+ setIsFormOpen(false);
+ setSelectedRecord(null);
+
+ // Trigger table refresh
+ setRefreshKey(prev => prev + 1);
+ }, []);
+
+ // Handle form cancellation
+ const handleFormCancel = useCallback(() => {
+ setIsFormOpen(false);
+ setSelectedRecord(null);
+ }, []);
+
+ // Handle refresh
+ const handleRefresh = useCallback(() => {
+ setRefreshKey(prev => prev + 1);
+ }, []);
+
+ // Build table schema
+ const tableSchema: ObjectTableSchema = {
+ type: 'object-table',
+ objectName: schema.objectName,
+ title: schema.table?.title,
+ description: schema.table?.description,
+ fields: schema.table?.fields,
+ columns: schema.table?.columns,
+ operations: {
+ ...operations,
+ create: false, // Create is handled by the view's create button
+ },
+ defaultFilters: schema.table?.defaultFilters,
+ defaultSort: schema.table?.defaultSort,
+ pageSize: schema.table?.pageSize,
+ selectable: schema.table?.selectable,
+ className: schema.table?.className,
+ };
+
+ // Build form schema
+ const buildFormSchema = (): ObjectFormSchema => {
+ const recordId = selectedRecord ? (selectedRecord._id || selectedRecord.id) as string | number | undefined : undefined;
+
+ return {
+ type: 'object-form',
+ objectName: schema.objectName,
+ mode: formMode,
+ recordId,
+ title: schema.form?.title,
+ description: schema.form?.description,
+ fields: schema.form?.fields,
+ customFields: schema.form?.customFields,
+ groups: schema.form?.groups,
+ layout: schema.form?.layout,
+ columns: schema.form?.columns,
+ showSubmit: schema.form?.showSubmit,
+ submitText: schema.form?.submitText,
+ showCancel: schema.form?.showCancel,
+ cancelText: schema.form?.cancelText,
+ showReset: schema.form?.showReset,
+ initialValues: schema.form?.initialValues,
+ readOnly: schema.form?.readOnly || formMode === 'view',
+ className: schema.form?.className,
+ onSuccess: handleFormSuccess,
+ onCancel: handleFormCancel,
+ };
+ };
+
+ // Get form title based on mode
+ const getFormTitle = (): string => {
+ if (schema.form?.title) return schema.form.title;
+
+ const objectLabel = (objectSchema?.label as string) || schema.objectName;
+
+ switch (formMode) {
+ case 'create':
+ return `Create ${objectLabel}`;
+ case 'edit':
+ return `Edit ${objectLabel}`;
+ case 'view':
+ return `View ${objectLabel}`;
+ default:
+ return objectLabel;
+ }
+ };
+
+ // Render the form in a drawer
+ const renderDrawerForm = () => (
+
+
+
+ {getFormTitle()}
+ {schema.form?.description && (
+ {schema.form.description}
+ )}
+
+
+
+
+
+
+ );
+
+ // Render the form in a modal
+ const renderModalForm = () => (
+
+ );
+
+ // Render toolbar
+ const renderToolbar = () => {
+ const showSearchBox = schema.showSearch !== false;
+ const showFiltersButton = schema.showFilters !== false;
+ const showCreateButton = schema.showCreate !== false && operations.create !== false;
+ const showRefreshButton = schema.showRefresh !== false;
+
+ // Don't render toolbar if no elements are shown
+ if (!showSearchBox && !showFiltersButton && !showCreateButton && !showRefreshButton) {
+ return null;
+ }
+
+ return (
+
+ {/* Main toolbar row */}
+
+ {/* Left side: Search */}
+
+ {showSearchBox && (
+
+
+ ) => setSearchQuery(e.target.value)}
+ className="pl-9"
+ />
+
+ )}
+
+
+ {/* Right side: Actions */}
+
+ {showFiltersButton && (
+
+ )}
+
+ {showRefreshButton && (
+
+ )}
+
+ {showCreateButton && (
+
+ )}
+
+
+
+ {/* Filter panel (shown when filters are active) */}
+ {showFilters && (
+
+
+ Filter functionality will be integrated with FilterBuilder component
+
+ {/* TODO: Integrate FilterBuilder component here */}
+
+ )}
+
+ );
+ };
+
+ return (
+
+ {/* Title and description */}
+ {(schema.title || schema.description) && (
+
+ {schema.title && (
+
{schema.title}
+ )}
+ {schema.description && (
+
{schema.description}
+ )}
+
+ )}
+
+ {/* Toolbar */}
+ {renderToolbar()}
+
+ {/* Table */}
+
+
+ {/* Form (drawer or modal) */}
+ {layout === 'drawer' && renderDrawerForm()}
+ {layout === 'modal' && renderModalForm()}
+
+ );
+};
diff --git a/packages/plugin-object/src/__tests__/ObjectView.test.tsx b/packages/plugin-object/src/__tests__/ObjectView.test.tsx
new file mode 100644
index 0000000..3f40cb0
--- /dev/null
+++ b/packages/plugin-object/src/__tests__/ObjectView.test.tsx
@@ -0,0 +1,361 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { ObjectView } from '../ObjectView';
+import type { ObjectViewSchema } from '@object-ui/types';
+import type { ObjectQLDataSource } from '@object-ui/data-objectql';
+
+// Mock child components
+vi.mock('../ObjectTable', () => ({
+ ObjectTable: ({ schema, onRowClick, onEdit, onDelete }: any) => (
+
+
{schema.objectName}
+
+
+
+
+ ),
+}));
+
+vi.mock('../ObjectForm', () => ({
+ ObjectForm: ({ schema }: any) => (
+
+
{schema.mode}
+
{schema.objectName}
+
+
+
+ ),
+}));
+
+// Mock UI components
+vi.mock('@object-ui/components/ui/dialog', () => ({
+ Dialog: ({ children, open, onOpenChange }: any) => (
+
+ {open && children}
+
+
+ ),
+ DialogContent: ({ children }: any) => {children}
,
+ DialogHeader: ({ children }: any) => {children}
,
+ DialogTitle: ({ children }: any) => {children}
,
+ DialogDescription: ({ children }: any) => {children}
,
+}));
+
+vi.mock('@object-ui/components/ui/drawer', () => ({
+ Drawer: ({ children, open, onOpenChange }: any) => (
+
+ {open && children}
+
+
+ ),
+ DrawerContent: ({ children }: any) => {children}
,
+ DrawerHeader: ({ children }: any) => {children}
,
+ DrawerTitle: ({ children }: any) => {children}
,
+ DrawerDescription: ({ children }: any) => {children}
,
+ DrawerClose: ({ children }: any) => {children}
,
+}));
+
+vi.mock('@object-ui/components/ui/button', () => ({
+ Button: ({ children, onClick, ...props }: any) => (
+
+ ),
+}));
+
+vi.mock('@object-ui/components/ui/input', () => ({
+ Input: (props: any) => ,
+}));
+
+describe('ObjectView', () => {
+ let mockDataSource: ObjectQLDataSource;
+ let mockSchema: ObjectViewSchema;
+
+ beforeEach(() => {
+ // Mock data source
+ mockDataSource = {
+ find: vi.fn().mockResolvedValue({
+ data: [
+ { _id: '1', name: 'John Doe', email: 'john@example.com' },
+ { _id: '2', name: 'Jane Smith', email: 'jane@example.com' },
+ ],
+ total: 2,
+ }),
+ findOne: vi.fn(),
+ create: vi.fn(),
+ update: vi.fn(),
+ delete: vi.fn(),
+ bulk: vi.fn(),
+ getObjectSchema: vi.fn().mockResolvedValue({
+ name: 'users',
+ label: 'Users',
+ fields: {
+ name: {
+ type: 'text',
+ label: 'Name',
+ required: true,
+ },
+ email: {
+ type: 'email',
+ label: 'Email',
+ required: true,
+ },
+ },
+ }),
+ } as any;
+
+ // Mock schema
+ mockSchema = {
+ type: 'object-view',
+ objectName: 'users',
+ title: 'User Management',
+ description: 'Manage users in your system',
+ };
+ });
+
+ it('renders the component with title and description', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('User Management')).toBeInTheDocument();
+ expect(screen.getByText('Manage users in your system')).toBeInTheDocument();
+ });
+ });
+
+ it('renders the object table', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('object-table')).toBeInTheDocument();
+ expect(screen.getByTestId('table-object-name')).toHaveTextContent('users');
+ });
+ });
+
+ it('shows search box by default', async () => {
+ render();
+
+ await waitFor(() => {
+ const searchInput = screen.getByPlaceholderText(/Search/i);
+ expect(searchInput).toBeInTheDocument();
+ });
+ });
+
+ it('shows create button by default', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Create')).toBeInTheDocument();
+ });
+ });
+
+ it('hides search box when showSearch is false', async () => {
+ const schema = { ...mockSchema, showSearch: false };
+ render();
+
+ await waitFor(() => {
+ expect(screen.queryByPlaceholderText(/Search/i)).not.toBeInTheDocument();
+ });
+ });
+
+ it('hides create button when showCreate is false', async () => {
+ const schema = { ...mockSchema, showCreate: false };
+ render();
+
+ await waitFor(() => {
+ expect(screen.queryByText('Create')).not.toBeInTheDocument();
+ });
+ });
+
+ it('opens drawer when create button is clicked (default layout)', async () => {
+ const user = userEvent.setup();
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Create')).toBeInTheDocument();
+ });
+
+ const createButton = screen.getByText('Create');
+ await user.click(createButton);
+
+ await waitFor(() => {
+ const drawer = screen.getByTestId('drawer');
+ expect(drawer).toHaveAttribute('data-open', 'true');
+ expect(screen.getByTestId('form-mode')).toHaveTextContent('create');
+ });
+ });
+
+ it('opens modal when create button is clicked (modal layout)', async () => {
+ const user = userEvent.setup();
+ const schema = { ...mockSchema, layout: 'modal' as const };
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Create')).toBeInTheDocument();
+ });
+
+ const createButton = screen.getByText('Create');
+ await user.click(createButton);
+
+ await waitFor(() => {
+ const dialog = screen.getByTestId('dialog');
+ expect(dialog).toHaveAttribute('data-open', 'true');
+ expect(screen.getByTestId('form-mode')).toHaveTextContent('create');
+ });
+ });
+
+ it('opens drawer when edit button is clicked', async () => {
+ const user = userEvent.setup();
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('object-table')).toBeInTheDocument();
+ });
+
+ const editButton = screen.getByTestId('edit-btn');
+ await user.click(editButton);
+
+ await waitFor(() => {
+ const drawer = screen.getByTestId('drawer');
+ expect(drawer).toHaveAttribute('data-open', 'true');
+ expect(screen.getByTestId('form-mode')).toHaveTextContent('edit');
+ });
+ });
+
+ it('opens drawer when row is clicked', async () => {
+ const user = userEvent.setup();
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('object-table')).toBeInTheDocument();
+ });
+
+ const rowButton = screen.getByTestId('row-click-btn');
+ await user.click(rowButton);
+
+ await waitFor(() => {
+ const drawer = screen.getByTestId('drawer');
+ expect(drawer).toHaveAttribute('data-open', 'true');
+ expect(screen.getByTestId('form-mode')).toHaveTextContent('view');
+ });
+ });
+
+ it('closes form when form submit is successful', async () => {
+ const user = userEvent.setup();
+ render();
+
+ // Open create form
+ await waitFor(() => {
+ expect(screen.getByText('Create')).toBeInTheDocument();
+ });
+ const createButton = screen.getByText('Create');
+ await user.click(createButton);
+
+ // Wait for form to open
+ await waitFor(() => {
+ expect(screen.getByTestId('drawer')).toHaveAttribute('data-open', 'true');
+ });
+
+ // Submit form
+ const submitButton = screen.getByTestId('form-submit');
+ await user.click(submitButton);
+
+ // Wait for drawer to close
+ await waitFor(() => {
+ expect(screen.getByTestId('drawer')).toHaveAttribute('data-open', 'false');
+ });
+ });
+
+ it('closes form when form cancel is clicked', async () => {
+ const user = userEvent.setup();
+ render();
+
+ // Open create form
+ await waitFor(() => {
+ expect(screen.getByText('Create')).toBeInTheDocument();
+ });
+ const createButton = screen.getByText('Create');
+ await user.click(createButton);
+
+ // Wait for form to open
+ await waitFor(() => {
+ expect(screen.getByTestId('drawer')).toHaveAttribute('data-open', 'true');
+ });
+
+ // Cancel form
+ const cancelButton = screen.getByTestId('form-cancel');
+ await user.click(cancelButton);
+
+ // Wait for drawer to close
+ await waitFor(() => {
+ expect(screen.getByTestId('drawer')).toHaveAttribute('data-open', 'false');
+ });
+ });
+
+ it('calls onNavigate for page layout mode', async () => {
+ const user = userEvent.setup();
+ const onNavigate = vi.fn();
+ const schema = {
+ ...mockSchema,
+ layout: 'page' as const,
+ onNavigate,
+ };
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Create')).toBeInTheDocument();
+ });
+
+ const createButton = screen.getByText('Create');
+ await user.click(createButton);
+
+ expect(onNavigate).toHaveBeenCalledWith('new', 'edit');
+ });
+
+ it('toggles filter panel when filter button is clicked', async () => {
+ const user = userEvent.setup();
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('Filters')).toBeInTheDocument();
+ });
+
+ // Filter panel should not be visible initially
+ expect(screen.queryByText(/Filter functionality will be integrated/)).not.toBeInTheDocument();
+
+ // Click filters button
+ const filtersButton = screen.getByText('Filters');
+ await user.click(filtersButton);
+
+ // Filter panel should now be visible
+ await waitFor(() => {
+ expect(screen.getByText(/Filter functionality will be integrated/)).toBeInTheDocument();
+ });
+
+ // Click filters button again
+ await user.click(filtersButton);
+
+ // Filter panel should be hidden
+ await waitFor(() => {
+ expect(screen.queryByText(/Filter functionality will be integrated/)).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/plugin-object/src/index.ts b/packages/plugin-object/src/index.ts
index eaaf8a3..59f6f90 100644
--- a/packages/plugin-object/src/index.ts
+++ b/packages/plugin-object/src/index.ts
@@ -22,9 +22,13 @@ export type { ObjectTableProps } from './ObjectTable';
export { ObjectForm } from './ObjectForm';
export type { ObjectFormProps } from './ObjectForm';
+export { ObjectView } from './ObjectView';
+export type { ObjectViewProps } from './ObjectView';
+
// Re-export related types from @object-ui/types
export type {
ObjectTableSchema,
ObjectFormSchema,
+ ObjectViewSchema,
ObjectQLComponentSchema,
} from '@object-ui/types';
diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts
index f383d12..4bf1852 100644
--- a/packages/types/src/index.ts
+++ b/packages/types/src/index.ts
@@ -258,6 +258,7 @@ export type {
export type {
ObjectTableSchema,
ObjectFormSchema,
+ ObjectViewSchema,
ObjectQLComponentSchema,
} from './objectql';
diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts
index cc50bc3..488112f 100644
--- a/packages/types/src/objectql.ts
+++ b/packages/types/src/objectql.ts
@@ -301,9 +301,99 @@ export interface ObjectFormSchema extends BaseSchema {
className?: string;
}
+/**
+ * ObjectView Schema
+ * A complete object management interface combining ObjectTable and ObjectForm.
+ * Provides list view with search, filters, and integrated create/edit dialogs.
+ */
+export interface ObjectViewSchema extends BaseSchema {
+ type: 'object-view';
+
+ /**
+ * ObjectQL object name (e.g., 'users', 'accounts', 'contacts')
+ */
+ objectName: string;
+
+ /**
+ * Optional title for the view
+ */
+ title?: string;
+
+ /**
+ * Optional description
+ */
+ description?: string;
+
+ /**
+ * Layout mode for create/edit operations
+ * - drawer: Side drawer (default, recommended for forms)
+ * - modal: Center modal dialog
+ * - page: Navigate to separate page (requires onNavigate handler)
+ * @default 'drawer'
+ */
+ layout?: 'drawer' | 'modal' | 'page';
+
+ /**
+ * Table configuration
+ * Inherits from ObjectTableSchema
+ */
+ table?: Partial>;
+
+ /**
+ * Form configuration
+ * Inherits from ObjectFormSchema
+ */
+ form?: Partial>;
+
+ /**
+ * Show search box
+ * @default true
+ */
+ showSearch?: boolean;
+
+ /**
+ * Show filters
+ * @default true
+ */
+ showFilters?: boolean;
+
+ /**
+ * Show create button
+ * @default true
+ */
+ showCreate?: boolean;
+
+ /**
+ * Show refresh button
+ * @default true
+ */
+ showRefresh?: boolean;
+
+ /**
+ * Enable/disable built-in operations
+ */
+ operations?: {
+ create?: boolean;
+ read?: boolean;
+ update?: boolean;
+ delete?: boolean;
+ };
+
+ /**
+ * Callback when navigating to detail page (page layout mode)
+ */
+ onNavigate?: (recordId: string | number, mode: 'view' | 'edit') => void;
+
+ /**
+ * Custom CSS class
+ */
+ className?: string;
+}
+
/**
* Union type of all ObjectQL component schemas
*/
export type ObjectQLComponentSchema =
| ObjectTableSchema
- | ObjectFormSchema;
+ | ObjectFormSchema
+ | ObjectViewSchema;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e18e1a0..dfa9355 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -219,6 +219,82 @@ importers:
specifier: ^7.3.1
version: 7.3.1(@types/node@25.0.9)(jiti@1.21.7)(lightningcss@1.30.2)
+ examples/object-view-demo:
+ dependencies:
+ '@object-ui/components':
+ specifier: workspace:*
+ version: link:../../packages/components
+ '@object-ui/core':
+ specifier: workspace:*
+ version: link:../../packages/core
+ '@object-ui/data-objectql':
+ specifier: workspace:*
+ version: link:../../packages/data-objectql
+ '@object-ui/plugin-object':
+ specifier: workspace:*
+ version: link:../../packages/plugin-object
+ '@object-ui/react':
+ specifier: workspace:*
+ version: link:../../packages/react
+ '@object-ui/types':
+ specifier: workspace:*
+ version: link:../../packages/types
+ lucide-react:
+ specifier: ^0.562.0
+ version: 0.562.0(react@19.2.3)
+ react:
+ specifier: 19.2.3
+ version: 19.2.3
+ react-dom:
+ specifier: 19.2.3
+ version: 19.2.3(react@19.2.3)
+ devDependencies:
+ '@eslint/js':
+ specifier: ^9.39.1
+ version: 9.39.2
+ '@types/node':
+ specifier: ^25.0.9
+ version: 25.0.9
+ '@types/react':
+ specifier: 19.0.6
+ version: 19.0.6
+ '@types/react-dom':
+ specifier: 19.0.3
+ version: 19.0.3(@types/react@19.0.6)
+ '@vitejs/plugin-react':
+ specifier: ^5.1.2
+ version: 5.1.2(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2))
+ autoprefixer:
+ specifier: ^10.4.23
+ version: 10.4.23(postcss@8.5.6)
+ eslint:
+ specifier: ^9.39.2
+ version: 9.39.2(jiti@2.6.1)
+ eslint-plugin-react-hooks:
+ specifier: ^7.0.1
+ version: 7.0.1(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-react-refresh:
+ specifier: ^0.4.24
+ version: 0.4.26(eslint@9.39.2(jiti@2.6.1))
+ globals:
+ specifier: ^16.5.0
+ version: 16.5.0
+ postcss:
+ specifier: ^8.5.6
+ version: 8.5.6
+ tailwindcss:
+ specifier: ^3.4.19
+ version: 3.4.19
+ typescript:
+ specifier: ~5.9.3
+ version: 5.9.3
+ typescript-eslint:
+ specifier: ^8.46.4
+ version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ vite:
+ specifier: ^7.3.1
+ version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)
+
packages/cli:
dependencies:
'@object-ui/components':