diff --git a/docs/BEST_PRACTICES.md b/docs/BEST_PRACTICES.md new file mode 100644 index 000000000..ceac0d36b --- /dev/null +++ b/docs/BEST_PRACTICES.md @@ -0,0 +1,636 @@ +# Object UI Best Practices + +This guide covers best practices for building applications with Object UI's JSON-driven approach. + +## Table of Contents + +- [Schema Design](#schema-design) +- [Performance](#performance) +- [Type Safety](#type-safety) +- [Maintainability](#maintainability) +- [API Integration](#api-integration) +- [Security](#security) +- [Testing](#testing) +- [Accessibility](#accessibility) + +## Schema Design + +### 1. Keep Schemas Modular and Reusable + +**❌ Bad:** +```json +{ + "type": "div", + "body": [ + { + "type": "card", + "title": "User 1", + "body": [ + { "type": "text", "content": "Name: John" }, + { "type": "text", "content": "Email: john@example.com" } + ] + }, + { + "type": "card", + "title": "User 2", + "body": [ + { "type": "text", "content": "Name: Jane" }, + { "type": "text", "content": "Email": "jane@example.com" } + ] + } + ] +} +``` + +**✅ Good:** +```json +{ + "type": "div", + "dataSource": { + "api": "/api/users" + }, + "body": { + "type": "grid", + "columns": 2, + "children": "${data.map(user => ({ type: 'card', title: user.name, body: [{ type: 'text', content: user.email }] }))}" + } +} +``` + +### 2. Use Semantic Types + +Choose the most appropriate component type for your content. + +**❌ Bad:** +```json +{ + "type": "div", + "className": "border p-4", + "body": "This is a warning" +} +``` + +**✅ Good:** +```json +{ + "type": "alert", + "variant": "warning", + "description": "This is a warning" +} +``` + +### 3. Leverage Conditional Rendering + +Use `visibleOn`, `hiddenOn`, and `disabledOn` for dynamic UIs. + +**✅ Good:** +```json +{ + "type": "button", + "label": "Delete", + "variant": "destructive", + "visibleOn": "${user.role === 'admin'}", + "disabledOn": "${item.status === 'locked'}" +} +``` + +### 4. Structure Complex Forms Logically + +Group related fields and use clear labels. + +**✅ Good:** +```json +{ + "type": "form", + "fields": [ + { + "name": "personalInfo", + "type": "group", + "label": "Personal Information", + "fields": [ + { "name": "firstName", "type": "input", "label": "First Name" }, + { "name": "lastName", "type": "input", "label": "Last Name" } + ] + }, + { + "name": "contactInfo", + "type": "group", + "label": "Contact Information", + "fields": [ + { "name": "email", "type": "input", "inputType": "email", "label": "Email" }, + { "name": "phone", "type": "input", "inputType": "tel", "label": "Phone" } + ] + } + ] +} +``` + +## Performance + +### 1. Use Data Fetching Wisely + +**❌ Bad** - Fetch data in every component: +```json +{ + "type": "div", + "body": [ + { + "type": "card", + "dataSource": { "api": "/api/stats" }, + "body": "..." + }, + { + "type": "card", + "dataSource": { "api": "/api/stats" }, + "body": "..." + } + ] +} +``` + +**✅ Good** - Fetch once at parent level: +```json +{ + "type": "div", + "dataSource": { "api": "/api/stats" }, + "body": [ + { + "type": "card", + "body": "${data.users}" + }, + { + "type": "card", + "body": "${data.orders}" + } + ] +} +``` + +### 2. Enable Caching for Static Data + +```json +{ + "dataSource": { + "api": "/api/countries", + "cache": { + "key": "countries-list", + "duration": 3600000, + "staleWhileRevalidate": true + } + } +} +``` + +### 3. Use Pagination for Large Lists + +```json +{ + "type": "crud", + "api": "/api/users", + "pagination": { + "enabled": true, + "pageSize": 20, + "pageSizeOptions": [10, 20, 50] + } +} +``` + +### 4. Optimize Polling Intervals + +```json +{ + "dataSource": { + "api": "/api/dashboard/stats", + "pollInterval": 30000, + "fetchOnMount": true + } +} +``` + +## Type Safety + +### 1. Use TypeScript for Schema Generation + +**✅ Good:** +```typescript +import { FormBuilder, input } from '@object-ui/core/builder'; + +const loginForm = new FormBuilder() + .field({ + name: 'email', + type: 'input', + inputType: 'email', + required: true + }) + .field({ + name: 'password', + type: 'input', + inputType: 'password', + required: true + }) + .submitLabel('Login') + .build(); +``` + +### 2. Validate Schemas Before Runtime + +```typescript +import { validateSchema, assertValidSchema } from '@object-ui/core/validation'; + +// Validate and get detailed errors +const result = validateSchema(schema); +if (!result.valid) { + console.error('Schema errors:', result.errors); +} + +// Or assert and throw on error +assertValidSchema(schema); +``` + +### 3. Use Schema Version for Compatibility + +```json +{ + "$schema": "https://objectui.org/schema/v1", + "type": "page", + "body": [...] +} +``` + +## Maintainability + +### 1. Use Descriptive IDs and Test IDs + +**✅ Good:** +```json +{ + "type": "button", + "id": "submit-login-button", + "testId": "login-submit", + "label": "Login" +} +``` + +### 2. Document Complex Expressions + +```json +{ + "type": "text", + "content": "${data.users.filter(u => u.active && u.verified).length}", + "description": "Count of active and verified users" +} +``` + +### 3. Use Constants for Repeated Values + +Instead of: +```json +{ + "api": "/api/v1/users", + "operations": { + "create": { "api": "/api/v1/users" }, + "update": { "api": "/api/v1/users/${id}" } + } +} +``` + +Use environment variables or configuration: +```json +{ + "api": "${env.API_BASE}/users", + "operations": { + "create": { "api": "${env.API_BASE}/users" }, + "update": { "api": "${env.API_BASE}/users/${id}" } + } +} +``` + +### 4. Organize Large Schemas + +Split large schemas into multiple files: + +```typescript +// schemas/users/list.json +// schemas/users/form.json +// schemas/users/detail.json + +import userList from './schemas/users/list.json'; +import userForm from './schemas/users/form.json'; +``` + +## API Integration + +### 1. Always Handle Errors + +**✅ Good:** +```json +{ + "onClick": { + "type": "api", + "api": { + "request": { + "url": "/api/action", + "method": "POST" + }, + "successMessage": "Action completed successfully!", + "errorMessage": "Failed to complete action. Please try again.", + "showLoading": true + } + } +} +``` + +### 2. Use Confirmation for Destructive Actions + +**✅ Good:** +```json +{ + "type": "action", + "label": "Delete", + "level": "danger", + "confirmText": "Are you sure you want to delete this item? This action cannot be undone.", + "api": "/api/items/${id}", + "method": "DELETE" +} +``` + +### 3. Provide User Feedback + +```json +{ + "api": { + "request": { + "url": "/api/save", + "method": "POST" + }, + "showLoading": true, + "successMessage": "Changes saved successfully!", + "errorMessage": "Failed to save changes", + "reload": true + } +} +``` + +### 4. Transform Data at the Source + +**✅ Good:** +```json +{ + "dataSource": { + "api": "/api/products", + "transform": "data => data.products.map(p => ({ ...p, displayName: `${p.name} (${p.sku})` }))" + } +} +``` + +## Security + +### 1. Never Expose Sensitive Data in Schemas + +**❌ Bad:** +```json +{ + "api": { + "url": "/api/users", + "headers": { + "Authorization": "******" + } + } +} +``` + +**✅ Good:** +```json +{ + "api": { + "url": "/api/users", + "headers": { + "Authorization": "${env.API_TOKEN}" + } + } +} +``` + +### 2. Validate User Input + +```json +{ + "name": "email", + "type": "input", + "inputType": "email", + "required": true, + "validation": { + "pattern": { + "value": "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$", + "message": "Please enter a valid email address" + } + } +} +``` + +### 3. Use HTTPS for APIs + +```json +{ + "api": "https://api.example.com/data" +} +``` + +### 4. Sanitize Dynamic Content + +When displaying user-generated content, use appropriate sanitization. + +## Testing + +### 1. Use Test IDs for E2E Tests + +```json +{ + "type": "button", + "testId": "submit-form", + "label": "Submit" +} +``` + +Then in tests: +```typescript +await page.getByTestId('submit-form').click(); +``` + +### 2. Validate Schemas in Tests + +```typescript +import { validateSchema } from '@object-ui/core/validation'; + +test('schema is valid', () => { + const result = validateSchema(mySchema); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); +}); +``` + +### 3. Test Dynamic Expressions + +```typescript +test('expression evaluates correctly', () => { + const schema = { + type: 'text', + content: '${user.name}' + }; + + const context = { user: { name: 'John' } }; + const result = evaluateExpression(schema.content, context); + expect(result).toBe('John'); +}); +``` + +## Accessibility + +### 1. Use Semantic HTML and ARIA Labels + +**✅ Good:** +```json +{ + "type": "button", + "label": "Close", + "ariaLabel": "Close dialog", + "icon": "x" +} +``` + +### 2. Provide Alternative Text for Images + +```json +{ + "type": "image", + "src": "/logo.png", + "alt": "Company Logo" +} +``` + +### 3. Ensure Keyboard Navigation + +```json +{ + "type": "dialog", + "closeOnEscape": true, + "showClose": true +} +``` + +### 4. Use Appropriate Color Contrast + +Use Tailwind's semantic color classes for proper contrast: + +```json +{ + "type": "button", + "className": "bg-primary text-primary-foreground" +} +``` + +## Common Patterns + +### Dashboard Card + +```json +{ + "type": "card", + "className": "shadow-lg", + "body": [ + { + "type": "flex", + "justify": "between", + "align": "start", + "body": [ + { + "type": "div", + "body": [ + { + "type": "text", + "content": "Total Users", + "className": "text-sm font-medium text-muted-foreground" + }, + { + "type": "text", + "content": "${data.userCount}", + "className": "text-3xl font-bold" + } + ] + }, + { + "type": "icon", + "name": "users", + "className": "text-muted-foreground" + } + ] + } + ] +} +``` + +### Data Table with Actions + +```json +{ + "type": "crud", + "resource": "user", + "api": "/api/users", + "columns": [...], + "rowActions": [ + { + "type": "action", + "label": "Edit", + "icon": "edit" + }, + { + "type": "action", + "label": "Delete", + "icon": "trash", + "level": "danger", + "confirmText": "Delete this user?" + } + ] +} +``` + +### Multi-Step Form + +```json +{ + "type": "tabs", + "items": [ + { + "value": "step1", + "label": "Personal Info", + "content": { + "type": "form", + "fields": [...] + } + }, + { + "value": "step2", + "label": "Contact Info", + "content": { + "type": "form", + "fields": [...] + } + } + ] +} +``` + +## Summary + +- ✅ Keep schemas modular and reusable +- ✅ Use semantic component types +- ✅ Leverage conditional rendering +- ✅ Optimize data fetching and caching +- ✅ Use TypeScript for type safety +- ✅ Validate schemas before runtime +- ✅ Handle errors gracefully +- ✅ Never expose sensitive data +- ✅ Add test IDs for testing +- ✅ Follow accessibility guidelines + +Following these best practices will help you build maintainable, performant, and accessible applications with Object UI. diff --git a/docs/integration/api.md b/docs/integration/api.md new file mode 100644 index 000000000..4d2bd6738 --- /dev/null +++ b/docs/integration/api.md @@ -0,0 +1,484 @@ +# API Integration Guide + +Object UI provides powerful API integration capabilities, allowing components to fetch data, submit forms, and trigger actions through REST APIs. + +## Overview + +API integration in Object UI supports: + +- 🔄 **Automatic Data Fetching** - Components fetch data on mount or on demand +- 🔃 **Real-time Updates** - Polling for live data updates +- 📤 **Form Submission** - Submit forms to APIs with validation +- ⚡ **Event-Driven Actions** - Trigger API calls from user interactions +- 🔁 **Request/Response Transformation** - Transform data before sending or after receiving +- 🔒 **Authentication** - Send credentials and custom headers +- ⏱️ **Retry & Timeout** - Configure retry logic and timeouts +- 💾 **Caching** - Cache responses for better performance + +## Data Fetching + +### Basic Data Fetching + +Use `dataSource` to fetch data automatically: + +```json +{ + "type": "div", + "dataSource": { + "api": "https://api.example.com/users" + }, + "body": { + "type": "list", + "items": "${data}" + } +} +``` + +### Advanced Data Fetching + +```json +{ + "type": "div", + "dataSource": { + "api": { + "url": "https://api.example.com/users", + "method": "GET", + "headers": { + "Authorization": "Bearer ${env.token}" + }, + "params": { + "page": 1, + "limit": 10 + } + }, + "fetchOnMount": true, + "pollInterval": 30000, + "transform": "data => data.users", + "filter": "user => user.active", + "sort": { + "field": "name", + "order": "asc" + }, + "pagination": { + "page": 1, + "pageSize": 10, + "enabled": true + } + } +} +``` + +### Configuration Options + +| Property | Type | Description | +|----------|------|-------------| +| `api` | `string \| APIRequest` | API endpoint or full request config | +| `fetchOnMount` | `boolean` | Fetch data when component mounts (default: true) | +| `pollInterval` | `number` | Polling interval in milliseconds | +| `dependencies` | `string[]` | Variables to watch for refetching | +| `defaultData` | `any` | Default data before fetch completes | +| `transform` | `string` | Transform function for response data | +| `filter` | `string` | Filter function for data | +| `sort` | `object` | Sort configuration | +| `pagination` | `object` | Pagination configuration | + +## API Requests + +### Request Configuration + +```typescript +interface APIRequest { + url: string; // API endpoint (supports ${} variables) + method?: HTTPMethod; // GET, POST, PUT, DELETE, PATCH + headers?: object; // Request headers + data?: any; // Request body (for POST/PUT/PATCH) + params?: object; // Query parameters + timeout?: number; // Request timeout in ms + withCredentials?: boolean; // Send cookies + transformRequest?: string; // Transform before sending + transformResponse?: string; // Transform after receiving +} +``` + +### Example Requests + +**Simple GET:** +```json +{ + "api": { + "url": "/api/users", + "method": "GET" + } +} +``` + +**POST with Data:** +```json +{ + "api": { + "url": "/api/users", + "method": "POST", + "data": { + "name": "${form.name}", + "email": "${form.email}" + } + } +} +``` + +**With Headers:** +```json +{ + "api": { + "url": "/api/users", + "method": "GET", + "headers": { + "Authorization": "Bearer ${env.token}", + "X-Custom-Header": "value" + } + } +} +``` + +**With Query Parameters:** +```json +{ + "api": { + "url": "/api/users", + "method": "GET", + "params": { + "page": "${state.page}", + "limit": 10, + "search": "${state.search}" + } + } +} +``` + +## Event Handlers + +### API Event Handler + +Trigger API calls from user interactions: + +```json +{ + "type": "button", + "label": "Save", + "onClick": { + "type": "api", + "api": { + "request": { + "url": "/api/save", + "method": "POST", + "data": "${form}" + }, + "successMessage": "Saved successfully!", + "errorMessage": "Save failed", + "showLoading": true, + "reload": true, + "redirect": "/success" + } + } +} +``` + +### API Configuration + +```typescript +interface APIConfig { + request?: APIRequest; + onSuccess?: string; // Success handler expression + onError?: string; // Error handler expression + showLoading?: boolean; // Show loading indicator + successMessage?: string; // Success toast message + errorMessage?: string; // Error toast message + reload?: boolean; // Reload data after success + redirect?: string; // Redirect after success + close?: boolean; // Close dialog after success + retry?: { + maxAttempts?: number; + delay?: number; + retryOn?: number[]; // HTTP status codes to retry + }; + cache?: { + key?: string; + duration?: number; + staleWhileRevalidate?: boolean; + }; +} +``` + +## Form Submission + +### Basic Form Submission + +```json +{ + "type": "form", + "fields": [...], + "onSubmit": { + "type": "api", + "api": { + "request": { + "url": "/api/contact", + "method": "POST" + }, + "successMessage": "Message sent!", + "showLoading": true + } + } +} +``` + +### Form with Validation + +```json +{ + "type": "form", + "fields": [ + { + "name": "email", + "type": "input", + "inputType": "email", + "required": true, + "validation": { + "pattern": { + "value": "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$", + "message": "Invalid email format" + } + } + } + ], + "onSubmit": { + "type": "api", + "api": { + "request": { + "url": "/api/register", + "method": "POST" + }, + "successMessage": "Registration successful!", + "redirect": "/login" + } + } +} +``` + +## Variable Substitution + +Use `${}` syntax to reference variables in URLs, data, and messages: + +### Available Variables + +- `${data}` - Component data +- `${form}` - Form values +- `${state}` - Global application state +- `${user}` - Current user information +- `${env}` - Environment variables +- `${utils}` - Utility functions + +### Examples + +```json +{ + "url": "/api/users/${user.id}", + "data": { + "name": "${form.name}", + "email": "${form.email}", + "createdBy": "${user.id}" + }, + "successMessage": "Welcome, ${form.name}!" +} +``` + +## Response Transformation + +Transform API responses using JavaScript expressions: + +```json +{ + "dataSource": { + "api": "/api/github/repos/objectstack-ai/objectui", + "transform": "data => ({ stars: data.stargazers_count, forks: data.forks_count })" + } +} +``` + +Multiple transformations: + +```json +{ + "dataSource": { + "api": "/api/users", + "transform": "data => data.users", + "filter": "user => user.active && user.role === 'admin'", + "sort": { + "field": "createdAt", + "order": "desc" + } + } +} +``` + +## Error Handling + +### Basic Error Handling + +```json +{ + "onClick": { + "type": "api", + "api": { + "request": { + "url": "/api/action", + "method": "POST" + }, + "successMessage": "Success!", + "errorMessage": "Operation failed. Please try again." + } + } +} +``` + +### Custom Error Handler + +```json +{ + "onClick": { + "type": "api", + "api": { + "request": { + "url": "/api/action", + "method": "POST" + }, + "onError": "error => { console.error(error); showNotification(error.message); }" + } + } +} +``` + +### Retry Configuration + +```json +{ + "api": { + "request": { + "url": "/api/action", + "method": "POST" + }, + "retry": { + "maxAttempts": 3, + "delay": 1000, + "retryOn": [500, 502, 503, 504] + } + } +} +``` + +## Caching + +Cache API responses to improve performance: + +```json +{ + "dataSource": { + "api": "/api/users", + "cache": { + "key": "users-list", + "duration": 300000, + "staleWhileRevalidate": true + } + } +} +``` + +### Cache Options + +- `key` - Unique cache key +- `duration` - Cache duration in milliseconds +- `staleWhileRevalidate` - Return stale data while fetching fresh data + +## Polling + +Automatically refetch data at regular intervals: + +```json +{ + "dataSource": { + "api": "/api/dashboard/stats", + "pollInterval": 30000, + "fetchOnMount": true + } +} +``` + +## Authentication + +### Bearer Token + +```json +{ + "api": { + "url": "/api/protected", + "method": "GET", + "headers": { + "Authorization": "Bearer ${env.API_TOKEN}" + } + } +} +``` + +### Cookies + +```json +{ + "api": { + "url": "/api/protected", + "method": "GET", + "withCredentials": true + } +} +``` + +### Custom Headers + +```json +{ + "api": { + "url": "/api/protected", + "method": "GET", + "headers": { + "X-API-Key": "${env.API_KEY}", + "X-User-Id": "${user.id}" + } + } +} +``` + +## Best Practices + +1. **Use Environment Variables** - Store API keys and tokens in environment variables +2. **Handle Errors Gracefully** - Always provide error messages +3. **Show Loading States** - Use `showLoading: true` for better UX +4. **Cache When Appropriate** - Cache static or slow-changing data +5. **Use Transformations** - Transform data at the source, not in templates +6. **Validate Input** - Use form validation before submitting +7. **Provide Feedback** - Show success/error messages to users +8. **Use Retry Logic** - Retry transient failures automatically +9. **Optimize Polling** - Use reasonable polling intervals +10. **Secure APIs** - Always use HTTPS and authentication + +## Examples + +See these examples for complete implementations: + +- [User Management](../../examples/user-management) - Full CRUD with API integration +- [API Integration Demo](../../examples/api-integration) - Various API patterns +- [Dashboard](../../examples/dashboard) - Real-time data fetching + +## Related Documentation + +- [CRUD Protocol](./crud.md) +- [Event Handling](./events.md) +- [Expression System](./expressions.md) +- [Data Sources](../integration/data-sources.md) diff --git a/docs/protocol/crud.md b/docs/protocol/crud.md new file mode 100644 index 000000000..81f98012b --- /dev/null +++ b/docs/protocol/crud.md @@ -0,0 +1,499 @@ +# CRUD Protocol Specification + +The CRUD (Create, Read, Update, Delete) protocol provides a complete data management interface through JSON schema. + +## Overview + +The CRUD protocol simplifies building data management interfaces by providing a declarative way to define tables, forms, filters, and operations. Instead of writing hundreds of lines of React code, you can define a complete CRUD interface in a single JSON schema. + +## Basic Structure + +```json +{ + "type": "crud", + "resource": "user", + "api": "/api/users", + "columns": [...], + "fields": [...], + "operations": {...}, + "toolbar": {...}, + "filters": [...], + "pagination": {...} +} +``` + +## Schema Properties + +### Core Properties + +| Property | Type | Required | Description | +|----------|------|----------|-------------| +| `type` | `string` | ✅ | Must be `"crud"` | +| `resource` | `string` | ❌ | Resource name (e.g., "user", "product") | +| `api` | `string` | ✅ | Base API endpoint | +| `title` | `string` | ❌ | Table title | +| `description` | `string` | ❌ | Table description | + +### Columns Configuration + +Defines how data is displayed in the table: + +```typescript +interface TableColumn { + name: string; // Field name + label?: string; // Display label + type?: string; // Column type (text, badge, date, image, etc.) + width?: number; // Column width in pixels + sortable?: boolean; // Enable sorting + searchable?: boolean; // Include in search + format?: string; // Format string (for dates, numbers) + render?: SchemaNode; // Custom render schema +} +``` + +**Example:** + +```json +{ + "columns": [ + { + "name": "id", + "label": "ID", + "type": "text", + "width": 80, + "sortable": true + }, + { + "name": "avatar", + "label": "Avatar", + "type": "image", + "width": 60 + }, + { + "name": "name", + "label": "Name", + "type": "text", + "sortable": true, + "searchable": true + }, + { + "name": "status", + "label": "Status", + "type": "badge", + "sortable": true + }, + { + "name": "createdAt", + "label": "Created", + "type": "date", + "format": "YYYY-MM-DD HH:mm", + "sortable": true + } + ] +} +``` + +### Fields Configuration + +Defines form fields for create/edit operations: + +```json +{ + "fields": [ + { + "name": "name", + "label": "Full Name", + "type": "input", + "required": true, + "placeholder": "Enter name" + }, + { + "name": "email", + "label": "Email", + "type": "input", + "inputType": "email", + "required": true + }, + { + "name": "role", + "label": "Role", + "type": "select", + "options": [ + { "label": "Admin", "value": "admin" }, + { "label": "User", "value": "user" } + ] + } + ] +} +``` + +### Operations Configuration + +Controls which CRUD operations are enabled: + +```typescript +interface CRUDOperation { + enabled?: boolean; + label?: string; + icon?: string; + api?: string; + method?: HTTPMethod; + confirmText?: string; + successMessage?: string; +} +``` + +**Example:** + +```json +{ + "operations": { + "create": { + "enabled": true, + "label": "Add User", + "icon": "plus", + "api": "/api/users", + "method": "POST", + "successMessage": "User created successfully!" + }, + "update": { + "enabled": true, + "label": "Edit", + "api": "/api/users/${id}", + "method": "PUT", + "successMessage": "User updated!" + }, + "delete": { + "enabled": true, + "label": "Delete", + "api": "/api/users/${id}", + "method": "DELETE", + "confirmText": "Delete this user?", + "successMessage": "User deleted!" + }, + "export": { + "enabled": true, + "label": "Export", + "api": "/api/users/export" + } + } +} +``` + +### Toolbar Configuration + +Customizes the toolbar appearance and actions: + +```json +{ + "toolbar": { + "showCreate": true, + "showRefresh": true, + "showExport": true, + "showImport": false, + "showFilter": true, + "showSearch": true, + "actions": [ + { + "type": "action", + "label": "Import CSV", + "icon": "upload", + "variant": "outline" + } + ] + } +} +``` + +### Filters Configuration + +Defines available filters: + +```json +{ + "filters": [ + { + "name": "role", + "label": "Role", + "type": "select", + "operator": "equals", + "options": [ + { "label": "All", "value": "" }, + { "label": "Admin", "value": "admin" }, + { "label": "User", "value": "user" } + ] + }, + { + "name": "status", + "label": "Status", + "type": "select", + "operator": "equals", + "options": [...] + }, + { + "name": "createdAt", + "label": "Created Date", + "type": "date-range", + "operator": "between" + } + ] +} +``` + +### Pagination Configuration + +Controls pagination behavior: + +```json +{ + "pagination": { + "enabled": true, + "pageSize": 20, + "pageSizeOptions": [10, 20, 50, 100], + "showTotal": true, + "showSizeChanger": true + } +} +``` + +### Batch Actions + +Actions that can be performed on multiple selected rows: + +```json +{ + "selectable": "multiple", + "batchActions": [ + { + "type": "action", + "label": "Activate Selected", + "icon": "check", + "level": "success", + "api": "/api/users/batch/activate", + "method": "POST", + "confirmText": "Activate ${count} users?", + "successMessage": "${count} users activated!" + }, + { + "type": "action", + "label": "Delete Selected", + "icon": "trash", + "level": "danger", + "api": "/api/users/batch/delete", + "method": "DELETE", + "confirmText": "Delete ${count} users?" + } + ] +} +``` + +### Row Actions + +Actions available for individual rows: + +```json +{ + "rowActions": [ + { + "type": "action", + "label": "View", + "icon": "eye", + "redirect": "/users/${id}" + }, + { + "type": "action", + "label": "Edit", + "icon": "edit" + }, + { + "type": "action", + "label": "Delete", + "icon": "trash", + "level": "danger", + "confirmText": "Delete this user?" + } + ] +} +``` + +## Display Modes + +The CRUD component supports multiple display modes: + +### Table Mode (Default) + +```json +{ + "mode": "table" +} +``` + +Traditional table view with columns and rows. + +### Grid Mode + +```json +{ + "mode": "grid", + "gridColumns": 3, + "cardTemplate": { + "type": "card", + "body": [ + { "type": "text", "content": "${item.name}" }, + { "type": "text", "content": "${item.email}" } + ] + } +} +``` + +Card-based grid layout. + +### List Mode + +```json +{ + "mode": "list", + "cardTemplate": { + "type": "div", + "className": "p-4 border-b", + "body": [...] + } +} +``` + +Vertical list layout. + +### Kanban Mode + +```json +{ + "mode": "kanban", + "kanbanGroupField": "status", + "kanbanColumns": [ + { "id": "todo", "title": "To Do", "color": "gray" }, + { "id": "in_progress", "title": "In Progress", "color": "blue" }, + { "id": "done", "title": "Done", "color": "green" } + ] +} +``` + +Kanban board layout with draggable cards. + +## API Contract + +The CRUD component expects specific API endpoints and response formats: + +### List Endpoint + +**Request:** `GET /api/users?page=1&pageSize=20&search=john&sort=name&order=asc` + +**Response:** +```json +{ + "data": [ + { + "id": 1, + "name": "John Doe", + "email": "john@example.com", + ... + } + ], + "total": 100, + "page": 1, + "pageSize": 20 +} +``` + +### Create Endpoint + +**Request:** `POST /api/users` +```json +{ + "name": "John Doe", + "email": "john@example.com", + ... +} +``` + +**Response:** +```json +{ + "id": 1, + "name": "John Doe", + "email": "john@example.com", + ... +} +``` + +### Read Endpoint + +**Request:** `GET /api/users/1` + +**Response:** +```json +{ + "id": 1, + "name": "John Doe", + "email": "john@example.com", + ... +} +``` + +### Update Endpoint + +**Request:** `PUT /api/users/1` +```json +{ + "name": "John Smith", + ... +} +``` + +**Response:** +```json +{ + "id": 1, + "name": "John Smith", + ... +} +``` + +### Delete Endpoint + +**Request:** `DELETE /api/users/1` + +**Response:** +```json +{ + "success": true +} +``` + +## Variable Substitution + +URLs and messages support variable substitution using `${}` syntax: + +- `${id}` - Current record ID +- `${count}` - Selected items count (for batch actions) +- `${item.field}` - Field value from current record + +**Examples:** + +```json +{ + "api": "/api/users/${id}", + "confirmText": "Delete ${item.name}?", + "successMessage": "${count} users activated!" +} +``` + +## Complete Example + +See [examples/user-management](../../examples/user-management) for a complete working example. + +## Related + +- [Action Schema](./action.md) +- [Detail Schema](./detail.md) +- [API Integration](../integration/api.md) +- [Event Handling](./events.md) diff --git a/examples/README.md b/examples/README.md index f773c96d0..8b87d5307 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,6 +14,8 @@ These examples demonstrate the new JSON project specification - pure JSON schema | [**dashboard**](./dashboard) | Analytics dashboard | ⭐⭐ Intermediate | Metrics, activity feeds, grids | | [**data-display**](./data-display) | Data visualization patterns | ⭐⭐ Intermediate | Lists, profiles, badges, progress | | [**landing-page**](./landing-page) | Marketing landing page | ⭐⭐⭐ Advanced | Hero sections, CTAs, full layouts | +| [**user-management**](./user-management) | Complete CRUD interface | ⭐⭐⭐ Advanced | Full CRUD, filters, pagination, batch actions | +| [**api-integration**](./api-integration) | API integration patterns | ⭐⭐⭐ Advanced | Data fetching, events, dynamic data | | [**cli-demo**](./cli-demo) | CLI demonstration | ⭐ Beginner | Bilingual form, gradient backgrounds | ### 🔧 Integration Examples diff --git a/examples/api-integration/app.json b/examples/api-integration/app.json new file mode 100644 index 000000000..1bb647fc1 --- /dev/null +++ b/examples/api-integration/app.json @@ -0,0 +1,366 @@ +{ + "$schema": "https://objectui.org/schema/v1", + "type": "page", + "title": "API Integration Demo", + "description": "Demonstrating dynamic data fetching, API calls, and event handling", + "body": [ + { + "type": "card", + "title": "🔄 Real-time Data Fetching", + "description": "Components that automatically fetch and display data from APIs", + "className": "mb-6", + "body": { + "type": "div", + "className": "p-6 space-y-4", + "dataSource": { + "api": "https://api.github.com/repos/objectstack-ai/objectui", + "fetchOnMount": true, + "pollInterval": 30000, + "transform": "data => ({ stars: data.stargazers_count, forks: data.forks_count, issues: data.open_issues_count })" + }, + "body": [ + { + "type": "div", + "className": "grid grid-cols-3 gap-4", + "body": [ + { + "type": "card", + "className": "text-center", + "body": [ + { + "type": "text", + "content": "${data.stars}", + "className": "text-3xl font-bold text-yellow-600" + }, + { + "type": "text", + "content": "⭐ Stars", + "className": "text-sm text-muted-foreground" + } + ] + }, + { + "type": "card", + "className": "text-center", + "body": [ + { + "type": "text", + "content": "${data.forks}", + "className": "text-3xl font-bold text-blue-600" + }, + { + "type": "text", + "content": "🔱 Forks", + "className": "text-sm text-muted-foreground" + } + ] + }, + { + "type": "card", + "className": "text-center", + "body": [ + { + "type": "text", + "content": "${data.issues}", + "className": "text-3xl font-bold text-red-600" + }, + { + "type": "text", + "content": "🐛 Issues", + "className": "text-sm text-muted-foreground" + } + ] + } + ] + }, + { + "type": "text", + "content": "Data updates every 30 seconds automatically", + "className": "text-xs text-muted-foreground text-center" + } + ] + } + }, + { + "type": "card", + "title": "🎯 Interactive Actions", + "description": "Buttons with API calls and event handlers", + "className": "mb-6", + "body": { + "type": "div", + "className": "p-6 space-y-4", + "body": [ + { + "type": "div", + "className": "flex gap-3", + "body": [ + { + "type": "button", + "label": "API Call", + "icon": "zap", + "onClick": { + "type": "api", + "api": { + "request": { + "url": "/api/test", + "method": "POST", + "data": { + "message": "Hello from Object UI!" + } + }, + "successMessage": "API call successful!", + "errorMessage": "API call failed", + "showLoading": true + } + } + }, + { + "type": "button", + "label": "Show Dialog", + "icon": "message-square", + "variant": "outline", + "onClick": { + "type": "dialog", + "dialog": { + "type": "alert", + "title": "Hello!", + "content": "This dialog was triggered by a button click event" + } + } + }, + { + "type": "button", + "label": "Show Toast", + "icon": "bell", + "variant": "outline", + "onClick": { + "type": "toast", + "toast": { + "type": "success", + "message": "Toast notification triggered!", + "duration": 3000 + } + } + }, + { + "type": "button", + "label": "Navigate", + "icon": "external-link", + "variant": "outline", + "onClick": { + "type": "navigation", + "navigate": { + "to": "/users", + "type": "push" + } + } + } + ] + } + ] + } + }, + { + "type": "card", + "title": "📝 Form with API Submission", + "description": "Forms that submit data to APIs with validation", + "className": "mb-6", + "body": { + "type": "div", + "className": "p-6", + "body": { + "type": "form", + "fields": [ + { + "name": "name", + "label": "Your Name", + "type": "input", + "placeholder": "Enter your name", + "required": true + }, + { + "name": "email", + "label": "Email Address", + "type": "input", + "inputType": "email", + "placeholder": "your@email.com", + "required": true + }, + { + "name": "message", + "label": "Message", + "type": "textarea", + "placeholder": "What would you like to tell us?", + "rows": 4, + "required": true + } + ], + "submitLabel": "Send Message", + "onSubmit": { + "type": "api", + "api": { + "request": { + "url": "/api/contact", + "method": "POST" + }, + "successMessage": "Message sent successfully!", + "errorMessage": "Failed to send message", + "showLoading": true, + "reload": false, + "close": false + } + } + } + } + }, + { + "type": "card", + "title": "🔀 Conditional Rendering", + "description": "Components that show/hide based on data conditions", + "className": "mb-6", + "body": { + "type": "div", + "className": "p-6 space-y-4", + "body": [ + { + "type": "select", + "name": "userType", + "label": "User Type", + "options": [ + { "label": "Select a type", "value": "" }, + { "label": "Admin", "value": "admin" }, + { "label": "User", "value": "user" }, + { "label": "Guest", "value": "guest" } + ] + }, + { + "type": "alert", + "variant": "default", + "title": "Admin Access", + "description": "You have full administrative privileges", + "visibleOn": "${form.userType === 'admin'}" + }, + { + "type": "alert", + "variant": "default", + "title": "Standard User", + "description": "You have standard user access", + "visibleOn": "${form.userType === 'user'}" + }, + { + "type": "alert", + "variant": "destructive", + "title": "Guest Access", + "description": "You have limited guest access", + "visibleOn": "${form.userType === 'guest'}" + }, + { + "type": "button", + "label": "Delete Account", + "variant": "destructive", + "visibleOn": "${form.userType === 'admin'}", + "onClick": { + "type": "dialog", + "dialog": { + "type": "confirm", + "title": "Confirm Deletion", + "content": "Are you sure you want to delete this account?", + "actions": [ + { + "label": "Cancel" + }, + { + "label": "Delete", + "handler": { + "type": "api", + "api": { + "request": { + "url": "/api/account", + "method": "DELETE" + }, + "successMessage": "Account deleted", + "close": true + } + } + } + ] + } + } + } + ] + } + }, + { + "type": "card", + "title": "⚡ Event Chaining", + "description": "Multiple actions triggered in sequence", + "className": "mb-6", + "body": { + "type": "div", + "className": "p-6", + "body": { + "type": "button", + "label": "Run Multi-Step Process", + "icon": "play", + "events": [ + { + "event": "click", + "type": "toast", + "toast": { + "type": "info", + "message": "Step 1: Starting process..." + } + }, + { + "event": "click", + "type": "api", + "api": { + "request": { + "url": "/api/step1", + "method": "POST" + }, + "showLoading": true + }, + "debounce": 1000 + }, + { + "event": "click", + "type": "toast", + "toast": { + "type": "success", + "message": "Process completed successfully!" + }, + "debounce": 2000 + } + ] + } + } + }, + { + "type": "card", + "title": "💡 Expression System", + "description": "Dynamic values using expressions", + "body": { + "type": "div", + "className": "p-6 space-y-3", + "body": [ + { + "type": "text", + "content": "Current time: ${new Date().toLocaleString()}", + "className": "text-sm" + }, + { + "type": "text", + "content": "User Agent: ${navigator.userAgent}", + "className": "text-xs text-muted-foreground" + }, + { + "type": "text", + "content": "Window Size: ${window.innerWidth}x${window.innerHeight}", + "className": "text-xs text-muted-foreground" + } + ] + } + } + ] +} diff --git a/examples/user-management/README.md b/examples/user-management/README.md new file mode 100644 index 000000000..3f1413b60 --- /dev/null +++ b/examples/user-management/README.md @@ -0,0 +1,185 @@ +# User Management Example + +A complete CRUD (Create, Read, Update, Delete) interface for managing users, built entirely with JSON schema. + +## Features + +This example demonstrates: + +- ✅ **Complete CRUD Operations** - Create, read, update, and delete users +- ✅ **Advanced Filtering** - Filter by role, status, and date range +- ✅ **Search & Sort** - Search across multiple fields with sortable columns +- ✅ **Pagination** - Configurable page sizes with total count display +- ✅ **Batch Operations** - Activate, deactivate, or delete multiple users at once +- ✅ **Row Actions** - View details, edit, reset password, or delete individual users +- ✅ **Form Validation** - Required fields, email validation, and file upload constraints +- ✅ **Confirmation Dialogs** - Safety prompts before destructive actions +- ✅ **Success Messages** - User feedback after successful operations +- ✅ **Custom Toolbar** - Additional actions like import CSV and bulk invite +- ✅ **Empty State** - Friendly message when no users exist + +## Schema Structure + +The example uses the new `CRUDSchema` type which provides a complete data management interface: + +```json +{ + "type": "crud", + "resource": "user", + "api": "/api/users", + "columns": [...], // Table columns configuration + "fields": [...], // Form fields for create/edit + "operations": {...}, // CRUD operation settings + "toolbar": {...}, // Toolbar configuration + "filters": [...], // Filter options + "pagination": {...}, // Pagination settings + "batchActions": [...], // Bulk operations + "rowActions": [...] // Per-row actions +} +``` + +## Running the Example + +### Using Object UI CLI + +```bash +# Install the CLI globally +npm install -g @object-ui/cli + +# Serve the example +cd examples/user-management +objectui serve app.json +``` + +Visit http://localhost:3000 to see the user management interface. + +### Using as a Template + +You can use this as a starting point for your own CRUD interfaces: + +1. Copy `app.json` to your project +2. Modify the `api` endpoints to match your backend +3. Customize the `columns` and `fields` for your data model +4. Adjust `operations`, `filters`, and `actions` as needed + +## Customization + +### Changing the Data Model + +Edit the `columns` array to define what fields are displayed: + +```json +{ + "columns": [ + { + "name": "fieldName", + "label": "Display Label", + "type": "text|badge|date|image", + "sortable": true, + "searchable": true + } + ] +} +``` + +### Modifying Operations + +Enable/disable operations or customize their behavior: + +```json +{ + "operations": { + "create": { + "enabled": true, + "label": "Add User", + "api": "/api/users", + "method": "POST", + "successMessage": "User created!" + }, + "delete": { + "enabled": true, + "confirmText": "Delete this user?", + "successMessage": "User deleted!" + } + } +} +``` + +### Adding Custom Actions + +Add custom toolbar or row actions: + +```json +{ + "toolbar": { + "actions": [ + { + "type": "action", + "label": "Custom Action", + "icon": "star", + "api": "/api/custom-endpoint", + "method": "POST" + } + ] + } +} +``` + +## API Integration + +This example expects a REST API with the following endpoints: + +- `GET /api/users` - List users (with pagination, filtering, sorting) +- `POST /api/users` - Create a new user +- `GET /api/users/:id` - Get user details +- `PUT /api/users/:id` - Update a user +- `DELETE /api/users/:id` - Delete a user +- `GET /api/users/export` - Export users +- `POST /api/users/batch/activate` - Batch activate users +- `POST /api/users/batch/deactivate` - Batch deactivate users +- `DELETE /api/users/batch/delete` - Batch delete users +- `POST /api/users/:id/reset-password` - Reset user password + +### Request/Response Format + +**List Users (GET /api/users)** + +Query Parameters: +- `page` - Page number (default: 1) +- `pageSize` - Results per page (default: 20) +- `search` - Search query +- `sort` - Sort field +- `order` - Sort order (asc/desc) +- `role` - Filter by role +- `status` - Filter by status + +Response: +```json +{ + "data": [ + { + "id": 1, + "name": "John Doe", + "email": "john@example.com", + "role": "admin", + "status": "active", + "avatar": "https://...", + "createdAt": "2024-01-15T10:00:00Z" + } + ], + "total": 100, + "page": 1, + "pageSize": 20 +} +``` + +## Learn More + +- [Object UI Documentation](https://www.objectui.org) +- [CRUD Schema Reference](../../docs/protocol/crud.md) +- [API Integration Guide](../../docs/integration/api.md) +- [More Examples](../) + +## License + +MIT diff --git a/examples/user-management/app.json b/examples/user-management/app.json new file mode 100644 index 000000000..a5a23a91b --- /dev/null +++ b/examples/user-management/app.json @@ -0,0 +1,322 @@ +{ + "$schema": "https://objectui.org/schema/v1", + "type": "page", + "title": "User Management", + "description": "Complete CRUD interface for managing users", + "body": [ + { + "type": "crud", + "resource": "user", + "api": "/api/users", + "title": "Users", + "description": "Manage system users and their permissions", + "columns": [ + { + "name": "id", + "label": "ID", + "type": "text", + "width": 80, + "sortable": true + }, + { + "name": "avatar", + "label": "Avatar", + "type": "image", + "width": 60 + }, + { + "name": "name", + "label": "Name", + "type": "text", + "sortable": true, + "searchable": true + }, + { + "name": "email", + "label": "Email", + "type": "text", + "sortable": true, + "searchable": true + }, + { + "name": "role", + "label": "Role", + "type": "badge", + "sortable": true + }, + { + "name": "status", + "label": "Status", + "type": "badge", + "sortable": true + }, + { + "name": "createdAt", + "label": "Created At", + "type": "date", + "format": "YYYY-MM-DD HH:mm", + "sortable": true + } + ], + "fields": [ + { + "name": "name", + "label": "Full Name", + "type": "input", + "required": true, + "placeholder": "Enter full name" + }, + { + "name": "email", + "label": "Email Address", + "type": "input", + "inputType": "email", + "required": true, + "placeholder": "user@example.com" + }, + { + "name": "password", + "label": "Password", + "type": "input", + "inputType": "password", + "required": true, + "placeholder": "Enter password", + "description": "Minimum 8 characters with letters and numbers" + }, + { + "name": "role", + "label": "Role", + "type": "select", + "required": true, + "options": [ + { "label": "Admin", "value": "admin" }, + { "label": "Editor", "value": "editor" }, + { "label": "Viewer", "value": "viewer" } + ] + }, + { + "name": "status", + "label": "Status", + "type": "radio-group", + "required": true, + "defaultValue": "active", + "options": [ + { "label": "Active", "value": "active" }, + { "label": "Inactive", "value": "inactive" }, + { "label": "Suspended", "value": "suspended" } + ] + }, + { + "name": "bio", + "label": "Biography", + "type": "textarea", + "placeholder": "Brief description about the user", + "rows": 4 + }, + { + "name": "avatar", + "label": "Avatar", + "type": "file-upload", + "accept": "image/*", + "maxSize": 2097152, + "description": "Upload a profile picture (max 2MB)" + } + ], + "operations": { + "create": { + "enabled": true, + "label": "Add User", + "icon": "plus", + "api": "/api/users", + "method": "POST", + "successMessage": "User created successfully!" + }, + "read": { + "enabled": true, + "api": "/api/users/${id}", + "method": "GET" + }, + "update": { + "enabled": true, + "label": "Edit", + "icon": "edit", + "api": "/api/users/${id}", + "method": "PUT", + "successMessage": "User updated successfully!" + }, + "delete": { + "enabled": true, + "label": "Delete", + "icon": "trash", + "api": "/api/users/${id}", + "method": "DELETE", + "confirmText": "Are you sure you want to delete this user?", + "successMessage": "User deleted successfully!" + }, + "export": { + "enabled": true, + "label": "Export Users", + "icon": "download", + "api": "/api/users/export", + "method": "GET" + } + }, + "toolbar": { + "showCreate": true, + "showRefresh": true, + "showExport": true, + "showImport": false, + "showFilter": true, + "showSearch": true, + "actions": [ + { + "type": "action", + "label": "Import CSV", + "icon": "upload", + "variant": "outline" + }, + { + "type": "action", + "label": "Bulk Invite", + "icon": "mail", + "variant": "outline" + } + ] + }, + "filters": [ + { + "name": "role", + "label": "Role", + "type": "select", + "operator": "equals", + "options": [ + { "label": "All Roles", "value": "" }, + { "label": "Admin", "value": "admin" }, + { "label": "Editor", "value": "editor" }, + { "label": "Viewer", "value": "viewer" } + ] + }, + { + "name": "status", + "label": "Status", + "type": "select", + "operator": "equals", + "options": [ + { "label": "All Status", "value": "" }, + { "label": "Active", "value": "active" }, + { "label": "Inactive", "value": "inactive" }, + { "label": "Suspended", "value": "suspended" } + ] + }, + { + "name": "createdAt", + "label": "Created Date", + "type": "date-range", + "operator": "between" + } + ], + "pagination": { + "enabled": true, + "pageSize": 20, + "pageSizeOptions": [10, 20, 50, 100], + "showTotal": true, + "showSizeChanger": true + }, + "defaultSort": "createdAt", + "defaultSortOrder": "desc", + "selectable": "multiple", + "batchActions": [ + { + "type": "action", + "label": "Activate Selected", + "icon": "check", + "level": "success", + "api": "/api/users/batch/activate", + "method": "POST", + "confirmText": "Activate ${count} selected users?", + "successMessage": "${count} users activated successfully!" + }, + { + "type": "action", + "label": "Deactivate Selected", + "icon": "x", + "level": "warning", + "api": "/api/users/batch/deactivate", + "method": "POST", + "confirmText": "Deactivate ${count} selected users?", + "successMessage": "${count} users deactivated successfully!" + }, + { + "type": "action", + "label": "Delete Selected", + "icon": "trash", + "level": "danger", + "api": "/api/users/batch/delete", + "method": "DELETE", + "confirmText": "Are you sure you want to delete ${count} selected users? This action cannot be undone.", + "successMessage": "${count} users deleted successfully!" + } + ], + "rowActions": [ + { + "type": "action", + "label": "View Details", + "icon": "eye", + "variant": "ghost", + "redirect": "/users/${id}" + }, + { + "type": "action", + "label": "Edit", + "icon": "edit", + "variant": "ghost" + }, + { + "type": "action", + "label": "Reset Password", + "icon": "key", + "variant": "ghost", + "api": "/api/users/${id}/reset-password", + "method": "POST", + "confirmText": "Send password reset email to this user?", + "successMessage": "Password reset email sent!" + }, + { + "type": "action", + "label": "Delete", + "icon": "trash", + "variant": "ghost", + "level": "danger", + "confirmText": "Are you sure you want to delete this user?" + } + ], + "mode": "table", + "emptyState": { + "type": "div", + "className": "text-center py-12", + "body": [ + { + "type": "text", + "content": "👤", + "className": "text-6xl mb-4" + }, + { + "type": "text", + "content": "No users found", + "className": "text-xl font-semibold mb-2" + }, + { + "type": "text", + "content": "Get started by creating your first user", + "className": "text-muted-foreground mb-4" + }, + { + "type": "button", + "label": "Add First User", + "icon": "plus" + } + ] + } + } + ] +} diff --git a/packages/core/package.json b/packages/core/package.json index 015c68ff4..310b6adc1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -11,6 +11,7 @@ "lint": "eslint ." }, "dependencies": { + "@object-ui/types": "workspace:*", "lodash": "^4.17.21", "zod": "^3.22.4" }, diff --git a/packages/core/src/builder/schema-builder.d.ts b/packages/core/src/builder/schema-builder.d.ts new file mode 100644 index 000000000..1cdb7fe3f --- /dev/null +++ b/packages/core/src/builder/schema-builder.d.ts @@ -0,0 +1,287 @@ +/** + * @object-ui/core - Schema Builder + * + * Fluent API for building schemas programmatically. + * Provides type-safe builder functions for common schema patterns. + * + * @module builder + * @packageDocumentation + */ +import type { BaseSchema, FormSchema, FormField, CRUDSchema, TableColumn, ActionSchema, ButtonSchema, InputSchema, CardSchema, GridSchema, FlexSchema } from '@object-ui/types'; +/** + * Base builder class + */ +declare class SchemaBuilder { + protected schema: any; + constructor(type: string); + /** + * Set the ID + */ + id(id: string): this; + /** + * Set the className + */ + className(className: string): this; + /** + * Set visibility + */ + visible(visible: boolean): this; + /** + * Set conditional visibility + */ + visibleOn(expression: string): this; + /** + * Set disabled state + */ + disabled(disabled: boolean): this; + /** + * Set test ID + */ + testId(testId: string): this; + /** + * Build the final schema + */ + build(): T; +} +/** + * Form builder + */ +export declare class FormBuilder extends SchemaBuilder { + constructor(); + /** + * Add a field to the form + */ + field(field: FormField): this; + /** + * Add multiple fields + */ + fields(fields: FormField[]): this; + /** + * Set default values + */ + defaultValues(values: Record): this; + /** + * Set submit label + */ + submitLabel(label: string): this; + /** + * Set form layout + */ + layout(layout: 'vertical' | 'horizontal'): this; + /** + * Set number of columns + */ + columns(columns: number): this; + /** + * Set submit handler + */ + onSubmit(handler: (data: Record) => void | Promise): this; +} +/** + * CRUD builder + */ +export declare class CRUDBuilder extends SchemaBuilder { + constructor(); + /** + * Set resource name + */ + resource(resource: string): this; + /** + * Set API endpoint + */ + api(api: string): this; + /** + * Set title + */ + title(title: string): this; + /** + * Set description + */ + description(description: string): this; + /** + * Add a column + */ + column(column: TableColumn): this; + /** + * Set all columns + */ + columns(columns: TableColumn[]): this; + /** + * Set form fields + */ + fields(fields: FormField[]): this; + /** + * Enable create operation + */ + enableCreate(label?: string): this; + /** + * Enable update operation + */ + enableUpdate(label?: string): this; + /** + * Enable delete operation + */ + enableDelete(label?: string, confirmText?: string): this; + /** + * Set pagination + */ + pagination(pageSize?: number): this; + /** + * Enable row selection + */ + selectable(mode?: 'single' | 'multiple'): this; + /** + * Add a batch action + */ + batchAction(action: ActionSchema): this; + /** + * Add a row action + */ + rowAction(action: ActionSchema): this; +} +/** + * Button builder + */ +export declare class ButtonBuilder extends SchemaBuilder { + constructor(); + /** + * Set button label + */ + label(label: string): this; + /** + * Set button variant + */ + variant(variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'): this; + /** + * Set button size + */ + size(size: 'default' | 'sm' | 'lg' | 'icon'): this; + /** + * Set button icon + */ + icon(icon: string): this; + /** + * Set click handler + */ + onClick(handler: () => void | Promise): this; + /** + * Set loading state + */ + loading(loading: boolean): this; +} +/** + * Input builder + */ +export declare class InputBuilder extends SchemaBuilder { + constructor(); + /** + * Set field name + */ + name(name: string): this; + /** + * Set label + */ + label(label: string): this; + /** + * Set placeholder + */ + placeholder(placeholder: string): this; + /** + * Set input type + */ + inputType(type: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url'): this; + /** + * Mark as required + */ + required(required?: boolean): this; + /** + * Set default value + */ + defaultValue(value: string | number): this; +} +/** + * Card builder + */ +export declare class CardBuilder extends SchemaBuilder { + constructor(); + /** + * Set card title + */ + title(title: string): this; + /** + * Set card description + */ + description(description: string): this; + /** + * Set card content + */ + content(content: BaseSchema | BaseSchema[]): this; + /** + * Set card variant + */ + variant(variant: 'default' | 'outline' | 'ghost'): this; + /** + * Make card hoverable + */ + hoverable(hoverable?: boolean): this; +} +/** + * Grid builder + */ +export declare class GridBuilder extends SchemaBuilder { + constructor(); + /** + * Set number of columns + */ + columns(columns: number): this; + /** + * Set gap + */ + gap(gap: number): this; + /** + * Add a child + */ + child(child: BaseSchema): this; + /** + * Set all children + */ + children(children: BaseSchema[]): this; +} +/** + * Flex builder + */ +export declare class FlexBuilder extends SchemaBuilder { + constructor(); + /** + * Set flex direction + */ + direction(direction: 'row' | 'col' | 'row-reverse' | 'col-reverse'): this; + /** + * Set justify content + */ + justify(justify: 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly'): this; + /** + * Set align items + */ + align(align: 'start' | 'end' | 'center' | 'baseline' | 'stretch'): this; + /** + * Set gap + */ + gap(gap: number): this; + /** + * Add a child + */ + child(child: BaseSchema): this; + /** + * Set all children + */ + children(children: BaseSchema[]): this; +} +export declare const form: () => FormBuilder; +export declare const crud: () => CRUDBuilder; +export declare const button: () => ButtonBuilder; +export declare const input: () => InputBuilder; +export declare const card: () => CardBuilder; +export declare const grid: () => GridBuilder; +export declare const flex: () => FlexBuilder; +export {}; diff --git a/packages/core/src/builder/schema-builder.js b/packages/core/src/builder/schema-builder.js new file mode 100644 index 000000000..e16686125 --- /dev/null +++ b/packages/core/src/builder/schema-builder.js @@ -0,0 +1,505 @@ +/** + * @object-ui/core - Schema Builder + * + * Fluent API for building schemas programmatically. + * Provides type-safe builder functions for common schema patterns. + * + * @module builder + * @packageDocumentation + */ +/** + * Base builder class + */ +class SchemaBuilder { + constructor(type) { + Object.defineProperty(this, "schema", { + enumerable: true, + configurable: true, + writable: true, + value: void 0 + }); + this.schema = { type }; + } + /** + * Set the ID + */ + id(id) { + this.schema.id = id; + return this; + } + /** + * Set the className + */ + className(className) { + this.schema.className = className; + return this; + } + /** + * Set visibility + */ + visible(visible) { + this.schema.visible = visible; + return this; + } + /** + * Set conditional visibility + */ + visibleOn(expression) { + this.schema.visibleOn = expression; + return this; + } + /** + * Set disabled state + */ + disabled(disabled) { + this.schema.disabled = disabled; + return this; + } + /** + * Set test ID + */ + testId(testId) { + this.schema.testId = testId; + return this; + } + /** + * Build the final schema + */ + build() { + return this.schema; + } +} +/** + * Form builder + */ +export class FormBuilder extends SchemaBuilder { + constructor() { + super('form'); + this.schema.fields = []; + } + /** + * Add a field to the form + */ + field(field) { + this.schema.fields = [...(this.schema.fields || []), field]; + return this; + } + /** + * Add multiple fields + */ + fields(fields) { + this.schema.fields = fields; + return this; + } + /** + * Set default values + */ + defaultValues(values) { + this.schema.defaultValues = values; + return this; + } + /** + * Set submit label + */ + submitLabel(label) { + this.schema.submitLabel = label; + return this; + } + /** + * Set form layout + */ + layout(layout) { + this.schema.layout = layout; + return this; + } + /** + * Set number of columns + */ + columns(columns) { + this.schema.columns = columns; + return this; + } + /** + * Set submit handler + */ + onSubmit(handler) { + this.schema.onSubmit = handler; + return this; + } +} +/** + * CRUD builder + */ +export class CRUDBuilder extends SchemaBuilder { + constructor() { + super('crud'); + this.schema.columns = []; + } + /** + * Set resource name + */ + resource(resource) { + this.schema.resource = resource; + return this; + } + /** + * Set API endpoint + */ + api(api) { + this.schema.api = api; + return this; + } + /** + * Set title + */ + title(title) { + this.schema.title = title; + return this; + } + /** + * Set description + */ + description(description) { + this.schema.description = description; + return this; + } + /** + * Add a column + */ + column(column) { + this.schema.columns = [...(this.schema.columns || []), column]; + return this; + } + /** + * Set all columns + */ + columns(columns) { + this.schema.columns = columns; + return this; + } + /** + * Set form fields + */ + fields(fields) { + this.schema.fields = fields; + return this; + } + /** + * Enable create operation + */ + enableCreate(label) { + if (!this.schema.operations) + this.schema.operations = {}; + this.schema.operations.create = { + enabled: true, + label: label || 'Create', + api: this.schema.api, + method: 'POST' + }; + return this; + } + /** + * Enable update operation + */ + enableUpdate(label) { + if (!this.schema.operations) + this.schema.operations = {}; + this.schema.operations.update = { + enabled: true, + label: label || 'Update', + api: `${this.schema.api}/\${id}`, + method: 'PUT' + }; + return this; + } + /** + * Enable delete operation + */ + enableDelete(label, confirmText) { + if (!this.schema.operations) + this.schema.operations = {}; + this.schema.operations.delete = { + enabled: true, + label: label || 'Delete', + api: `${this.schema.api}/\${id}`, + method: 'DELETE', + confirmText: confirmText || 'Are you sure?' + }; + return this; + } + /** + * Set pagination + */ + pagination(pageSize = 20) { + this.schema.pagination = { + enabled: true, + pageSize, + pageSizeOptions: [10, 20, 50, 100], + showTotal: true, + showSizeChanger: true + }; + return this; + } + /** + * Enable row selection + */ + selectable(mode = 'multiple') { + this.schema.selectable = mode; + return this; + } + /** + * Add a batch action + */ + batchAction(action) { + this.schema.batchActions = [...(this.schema.batchActions || []), action]; + return this; + } + /** + * Add a row action + */ + rowAction(action) { + this.schema.rowActions = [...(this.schema.rowActions || []), action]; + return this; + } +} +/** + * Button builder + */ +export class ButtonBuilder extends SchemaBuilder { + constructor() { + super('button'); + } + /** + * Set button label + */ + label(label) { + this.schema.label = label; + return this; + } + /** + * Set button variant + */ + variant(variant) { + this.schema.variant = variant; + return this; + } + /** + * Set button size + */ + size(size) { + this.schema.size = size; + return this; + } + /** + * Set button icon + */ + icon(icon) { + this.schema.icon = icon; + return this; + } + /** + * Set click handler + */ + onClick(handler) { + this.schema.onClick = handler; + return this; + } + /** + * Set loading state + */ + loading(loading) { + this.schema.loading = loading; + return this; + } +} +/** + * Input builder + */ +export class InputBuilder extends SchemaBuilder { + constructor() { + super('input'); + } + /** + * Set field name + */ + name(name) { + this.schema.name = name; + return this; + } + /** + * Set label + */ + label(label) { + this.schema.label = label; + return this; + } + /** + * Set placeholder + */ + placeholder(placeholder) { + this.schema.placeholder = placeholder; + return this; + } + /** + * Set input type + */ + inputType(type) { + this.schema.inputType = type; + return this; + } + /** + * Mark as required + */ + required(required = true) { + this.schema.required = required; + return this; + } + /** + * Set default value + */ + defaultValue(value) { + this.schema.defaultValue = value; + return this; + } +} +/** + * Card builder + */ +export class CardBuilder extends SchemaBuilder { + constructor() { + super('card'); + } + /** + * Set card title + */ + title(title) { + this.schema.title = title; + return this; + } + /** + * Set card description + */ + description(description) { + this.schema.description = description; + return this; + } + /** + * Set card content + */ + content(content) { + this.schema.content = content; + return this; + } + /** + * Set card variant + */ + variant(variant) { + this.schema.variant = variant; + return this; + } + /** + * Make card hoverable + */ + hoverable(hoverable = true) { + this.schema.hoverable = hoverable; + return this; + } +} +/** + * Grid builder + */ +export class GridBuilder extends SchemaBuilder { + constructor() { + super('grid'); + this.schema.children = []; + } + /** + * Set number of columns + */ + columns(columns) { + this.schema.columns = columns; + return this; + } + /** + * Set gap + */ + gap(gap) { + this.schema.gap = gap; + return this; + } + /** + * Add a child + */ + child(child) { + const children = Array.isArray(this.schema.children) ? this.schema.children : []; + this.schema.children = [...children, child]; + return this; + } + /** + * Set all children + */ + children(children) { + this.schema.children = children; + return this; + } +} +/** + * Flex builder + */ +export class FlexBuilder extends SchemaBuilder { + constructor() { + super('flex'); + this.schema.children = []; + } + /** + * Set flex direction + */ + direction(direction) { + this.schema.direction = direction; + return this; + } + /** + * Set justify content + */ + justify(justify) { + this.schema.justify = justify; + return this; + } + /** + * Set align items + */ + align(align) { + this.schema.align = align; + return this; + } + /** + * Set gap + */ + gap(gap) { + this.schema.gap = gap; + return this; + } + /** + * Add a child + */ + child(child) { + const children = Array.isArray(this.schema.children) ? this.schema.children : []; + this.schema.children = [...children, child]; + return this; + } + /** + * Set all children + */ + children(children) { + this.schema.children = children; + return this; + } +} +// Export factory functions +export const form = () => new FormBuilder(); +export const crud = () => new CRUDBuilder(); +export const button = () => new ButtonBuilder(); +export const input = () => new InputBuilder(); +export const card = () => new CardBuilder(); +export const grid = () => new GridBuilder(); +export const flex = () => new FlexBuilder(); diff --git a/packages/core/src/builder/schema-builder.ts b/packages/core/src/builder/schema-builder.ts new file mode 100644 index 000000000..788546949 --- /dev/null +++ b/packages/core/src/builder/schema-builder.ts @@ -0,0 +1,576 @@ +/** + * @object-ui/core - Schema Builder + * + * Fluent API for building schemas programmatically. + * Provides type-safe builder functions for common schema patterns. + * + * @module builder + * @packageDocumentation + */ + +import type { + BaseSchema, + FormSchema, + FormField, + CRUDSchema, + TableColumn, + ActionSchema, + ButtonSchema, + InputSchema, + CardSchema, + GridSchema, + FlexSchema +} from '@object-ui/types'; + +/** + * Base builder class + */ +class SchemaBuilder { + protected schema: any; + + constructor(type: string) { + this.schema = { type }; + } + + /** + * Set the ID + */ + id(id: string): this { + this.schema.id = id; + return this; + } + + /** + * Set the className + */ + className(className: string): this { + this.schema.className = className; + return this; + } + + /** + * Set visibility + */ + visible(visible: boolean): this { + this.schema.visible = visible; + return this; + } + + /** + * Set conditional visibility + */ + visibleOn(expression: string): this { + this.schema.visibleOn = expression; + return this; + } + + /** + * Set disabled state + */ + disabled(disabled: boolean): this { + this.schema.disabled = disabled; + return this; + } + + /** + * Set test ID + */ + testId(testId: string): this { + this.schema.testId = testId; + return this; + } + + /** + * Build the final schema + */ + build(): T { + return this.schema as T; + } +} + +/** + * Form builder + */ +export class FormBuilder extends SchemaBuilder { + constructor() { + super('form'); + this.schema.fields = []; + } + + /** + * Add a field to the form + */ + field(field: FormField): this { + this.schema.fields = [...(this.schema.fields || []), field]; + return this; + } + + /** + * Add multiple fields + */ + fields(fields: FormField[]): this { + this.schema.fields = fields; + return this; + } + + /** + * Set default values + */ + defaultValues(values: Record): this { + this.schema.defaultValues = values; + return this; + } + + /** + * Set submit label + */ + submitLabel(label: string): this { + this.schema.submitLabel = label; + return this; + } + + /** + * Set form layout + */ + layout(layout: 'vertical' | 'horizontal'): this { + this.schema.layout = layout; + return this; + } + + /** + * Set number of columns + */ + columns(columns: number): this { + this.schema.columns = columns; + return this; + } + + /** + * Set submit handler + */ + onSubmit(handler: (data: Record) => void | Promise): this { + this.schema.onSubmit = handler; + return this; + } +} + +/** + * CRUD builder + */ +export class CRUDBuilder extends SchemaBuilder { + constructor() { + super('crud'); + this.schema.columns = []; + } + + /** + * Set resource name + */ + resource(resource: string): this { + this.schema.resource = resource; + return this; + } + + /** + * Set API endpoint + */ + api(api: string): this { + this.schema.api = api; + return this; + } + + /** + * Set title + */ + title(title: string): this { + this.schema.title = title; + return this; + } + + /** + * Set description + */ + description(description: string): this { + this.schema.description = description; + return this; + } + + /** + * Add a column + */ + column(column: TableColumn): this { + this.schema.columns = [...(this.schema.columns || []), column]; + return this; + } + + /** + * Set all columns + */ + columns(columns: TableColumn[]): this { + this.schema.columns = columns; + return this; + } + + /** + * Set form fields + */ + fields(fields: FormField[]): this { + this.schema.fields = fields; + return this; + } + + /** + * Enable create operation + */ + enableCreate(label?: string): this { + if (!this.schema.operations) this.schema.operations = {}; + this.schema.operations.create = { + enabled: true, + label: label || 'Create', + api: this.schema.api, + method: 'POST' + }; + return this; + } + + /** + * Enable update operation + */ + enableUpdate(label?: string): this { + if (!this.schema.operations) this.schema.operations = {}; + this.schema.operations.update = { + enabled: true, + label: label || 'Update', + api: `${this.schema.api}/\${id}`, + method: 'PUT' + }; + return this; + } + + /** + * Enable delete operation + */ + enableDelete(label?: string, confirmText?: string): this { + if (!this.schema.operations) this.schema.operations = {}; + this.schema.operations.delete = { + enabled: true, + label: label || 'Delete', + api: `${this.schema.api}/\${id}`, + method: 'DELETE', + confirmText: confirmText || 'Are you sure?' + }; + return this; + } + + /** + * Set pagination + */ + pagination(pageSize: number = 20): this { + this.schema.pagination = { + enabled: true, + pageSize, + pageSizeOptions: [10, 20, 50, 100], + showTotal: true, + showSizeChanger: true + }; + return this; + } + + /** + * Enable row selection + */ + selectable(mode: 'single' | 'multiple' = 'multiple'): this { + this.schema.selectable = mode; + return this; + } + + /** + * Add a batch action + */ + batchAction(action: ActionSchema): this { + this.schema.batchActions = [...(this.schema.batchActions || []), action]; + return this; + } + + /** + * Add a row action + */ + rowAction(action: ActionSchema): this { + this.schema.rowActions = [...(this.schema.rowActions || []), action]; + return this; + } +} + +/** + * Button builder + */ +export class ButtonBuilder extends SchemaBuilder { + constructor() { + super('button'); + } + + /** + * Set button label + */ + label(label: string): this { + this.schema.label = label; + return this; + } + + /** + * Set button variant + */ + variant(variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link'): this { + this.schema.variant = variant; + return this; + } + + /** + * Set button size + */ + size(size: 'default' | 'sm' | 'lg' | 'icon'): this { + this.schema.size = size; + return this; + } + + /** + * Set button icon + */ + icon(icon: string): this { + this.schema.icon = icon; + return this; + } + + /** + * Set click handler + */ + onClick(handler: () => void | Promise): this { + this.schema.onClick = handler; + return this; + } + + /** + * Set loading state + */ + loading(loading: boolean): this { + this.schema.loading = loading; + return this; + } +} + +/** + * Input builder + */ +export class InputBuilder extends SchemaBuilder { + constructor() { + super('input'); + } + + /** + * Set field name + */ + name(name: string): this { + this.schema.name = name; + return this; + } + + /** + * Set label + */ + label(label: string): this { + this.schema.label = label; + return this; + } + + /** + * Set placeholder + */ + placeholder(placeholder: string): this { + this.schema.placeholder = placeholder; + return this; + } + + /** + * Set input type + */ + inputType(type: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url'): this { + this.schema.inputType = type; + return this; + } + + /** + * Mark as required + */ + required(required: boolean = true): this { + this.schema.required = required; + return this; + } + + /** + * Set default value + */ + defaultValue(value: string | number): this { + this.schema.defaultValue = value; + return this; + } +} + +/** + * Card builder + */ +export class CardBuilder extends SchemaBuilder { + constructor() { + super('card'); + } + + /** + * Set card title + */ + title(title: string): this { + this.schema.title = title; + return this; + } + + /** + * Set card description + */ + description(description: string): this { + this.schema.description = description; + return this; + } + + /** + * Set card content + */ + content(content: BaseSchema | BaseSchema[]): this { + this.schema.content = content; + return this; + } + + /** + * Set card variant + */ + variant(variant: 'default' | 'outline' | 'ghost'): this { + this.schema.variant = variant; + return this; + } + + /** + * Make card hoverable + */ + hoverable(hoverable: boolean = true): this { + this.schema.hoverable = hoverable; + return this; + } +} + +/** + * Grid builder + */ +export class GridBuilder extends SchemaBuilder { + constructor() { + super('grid'); + this.schema.children = []; + } + + /** + * Set number of columns + */ + columns(columns: number): this { + this.schema.columns = columns; + return this; + } + + /** + * Set gap + */ + gap(gap: number): this { + this.schema.gap = gap; + return this; + } + + /** + * Add a child + */ + child(child: BaseSchema): this { + const children = Array.isArray(this.schema.children) ? this.schema.children : []; + this.schema.children = [...children, child]; + return this; + } + + /** + * Set all children + */ + children(children: BaseSchema[]): this { + this.schema.children = children; + return this; + } +} + +/** + * Flex builder + */ +export class FlexBuilder extends SchemaBuilder { + constructor() { + super('flex'); + this.schema.children = []; + } + + /** + * Set flex direction + */ + direction(direction: 'row' | 'col' | 'row-reverse' | 'col-reverse'): this { + this.schema.direction = direction; + return this; + } + + /** + * Set justify content + */ + justify(justify: 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly'): this { + this.schema.justify = justify; + return this; + } + + /** + * Set align items + */ + align(align: 'start' | 'end' | 'center' | 'baseline' | 'stretch'): this { + this.schema.align = align; + return this; + } + + /** + * Set gap + */ + gap(gap: number): this { + this.schema.gap = gap; + return this; + } + + /** + * Add a child + */ + child(child: BaseSchema): this { + const children = Array.isArray(this.schema.children) ? this.schema.children : []; + this.schema.children = [...children, child]; + return this; + } + + /** + * Set all children + */ + children(children: BaseSchema[]): this { + this.schema.children = children; + return this; + } +} + +// Export factory functions +export const form = () => new FormBuilder(); +export const crud = () => new CRUDBuilder(); +export const button = () => new ButtonBuilder(); +export const input = () => new InputBuilder(); +export const card = () => new CardBuilder(); +export const grid = () => new GridBuilder(); +export const flex = () => new FlexBuilder(); diff --git a/packages/core/src/index.d.ts b/packages/core/src/index.d.ts index 3e73ea163..b3b222bcb 100644 --- a/packages/core/src/index.d.ts +++ b/packages/core/src/index.d.ts @@ -1,2 +1,4 @@ export * from './types'; export * from './registry/Registry'; +export * from './validation/schema-validator'; +export * from './builder/schema-builder'; diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 301d3a241..2651ed50d 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,5 +1,7 @@ export * from './types'; export * from './registry/Registry'; +export * from './validation/schema-validator'; +export * from './builder/schema-builder'; // export * from './data-scope'; // TODO // export * from './evaluator'; // TODO // export * from './validators'; // TODO diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 065f30e10..6a96f8063 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,7 @@ export * from './types'; export * from './registry/Registry'; +export * from './validation/schema-validator'; +export * from './builder/schema-builder'; // export * from './data-scope'; // TODO // export * from './evaluator'; // TODO // export * from './validators'; // TODO diff --git a/packages/core/src/registry/Registry.d.ts b/packages/core/src/registry/Registry.d.ts index f2b2dd0f7..0053d24f3 100644 --- a/packages/core/src/registry/Registry.d.ts +++ b/packages/core/src/registry/Registry.d.ts @@ -12,13 +12,26 @@ export type ComponentInput = { }[]; description?: string; advanced?: boolean; + inputType?: string; }; export type ComponentMeta = { label?: string; icon?: string; + category?: string; inputs?: ComponentInput[]; defaultProps?: Record; defaultChildren?: SchemaNode[]; + examples?: Record; + isContainer?: boolean; + resizable?: boolean; + resizeConstraints?: { + width?: boolean; + height?: boolean; + minWidth?: number; + maxWidth?: number; + minHeight?: number; + maxHeight?: number; + }; }; export type ComponentConfig = ComponentMeta & { type: string; diff --git a/packages/core/src/validation/schema-validator.d.ts b/packages/core/src/validation/schema-validator.d.ts new file mode 100644 index 000000000..de426df23 --- /dev/null +++ b/packages/core/src/validation/schema-validator.d.ts @@ -0,0 +1,87 @@ +/** + * @object-ui/core - Schema Validation + * + * Runtime validation utilities for Object UI schemas. + * These utilities help ensure schemas are valid before rendering. + * + * @module validation + * @packageDocumentation + */ +import type { BaseSchema } from '@object-ui/types'; +/** + * Validation error details + */ +export interface ValidationError { + path: string; + message: string; + type: 'error' | 'warning'; + code?: string; +} +/** + * Validation result + */ +export interface ValidationResult { + valid: boolean; + errors: ValidationError[]; + warnings: ValidationError[]; +} +/** + * Validate a complete schema + * + * @param schema - The schema to validate + * @param options - Validation options + * @returns Validation result with errors and warnings + * + * @example + * ```typescript + * const result = validateSchema({ + * type: 'form', + * fields: [ + * { name: 'email', type: 'input' } + * ] + * }); + * + * if (!result.valid) { + * console.error('Validation errors:', result.errors); + * } + * ``` + */ +export declare function validateSchema(schema: any, path?: string): ValidationResult; +/** + * Assert that a schema is valid, throwing an error if not + * + * @param schema - The schema to validate + * @throws Error if schema is invalid + * + * @example + * ```typescript + * try { + * assertValidSchema(schema); + * // Schema is valid, continue rendering + * } catch (error) { + * console.error('Invalid schema:', error.message); + * } + * ``` + */ +export declare function assertValidSchema(schema: any): asserts schema is BaseSchema; +/** + * Check if a value is a valid schema + * + * @param value - The value to check + * @returns True if the value is a valid schema + * + * @example + * ```typescript + * if (isValidSchema(data)) { + * renderSchema(data); + * } + * ``` + */ +export declare function isValidSchema(value: any): value is BaseSchema; +/** + * Get a human-readable error summary + * + * @param result - The validation result + * @returns Formatted error summary + */ +export declare function formatValidationErrors(result: ValidationResult): string; diff --git a/packages/core/src/validation/schema-validator.js b/packages/core/src/validation/schema-validator.js new file mode 100644 index 000000000..611e116b7 --- /dev/null +++ b/packages/core/src/validation/schema-validator.js @@ -0,0 +1,280 @@ +/** + * @object-ui/core - Schema Validation + * + * Runtime validation utilities for Object UI schemas. + * These utilities help ensure schemas are valid before rendering. + * + * @module validation + * @packageDocumentation + */ +/** + * Validation rules for base schema + */ +const BASE_SCHEMA_RULES = { + type: { + required: true, + validate: (value) => typeof value === 'string' && value.length > 0, + message: 'type must be a non-empty string' + }, + id: { + required: false, + validate: (value) => typeof value === 'string', + message: 'id must be a string' + }, + className: { + required: false, + validate: (value) => typeof value === 'string', + message: 'className must be a string' + }, + visible: { + required: false, + validate: (value) => typeof value === 'boolean', + message: 'visible must be a boolean' + }, + disabled: { + required: false, + validate: (value) => typeof value === 'boolean', + message: 'disabled must be a boolean' + } +}; +/** + * Validate a schema against base rules + */ +function validateBaseSchema(schema, path = 'schema') { + const errors = []; + if (!schema || typeof schema !== 'object') { + errors.push({ + path, + message: 'Schema must be an object', + type: 'error', + code: 'INVALID_SCHEMA' + }); + return errors; + } + // Validate required and optional properties + Object.entries(BASE_SCHEMA_RULES).forEach(([key, rule]) => { + const value = schema[key]; + if (rule.required && value === undefined) { + errors.push({ + path: `${path}.${key}`, + message: `${key} is required`, + type: 'error', + code: 'MISSING_REQUIRED' + }); + } + if (value !== undefined && !rule.validate(value)) { + errors.push({ + path: `${path}.${key}`, + message: rule.message, + type: 'error', + code: 'INVALID_TYPE' + }); + } + }); + return errors; +} +/** + * Validate CRUD schema specific properties + */ +function validateCRUDSchema(schema, path = 'schema') { + const errors = []; + if (schema.type === 'crud') { + // Check required properties for CRUD + if (!schema.columns || !Array.isArray(schema.columns)) { + errors.push({ + path: `${path}.columns`, + message: 'CRUD schema requires columns array', + type: 'error', + code: 'MISSING_COLUMNS' + }); + } + if (!schema.api && !schema.dataSource) { + errors.push({ + path: `${path}.api`, + message: 'CRUD schema requires api or dataSource', + type: 'warning', + code: 'MISSING_DATA_SOURCE' + }); + } + // Validate columns + if (schema.columns && Array.isArray(schema.columns)) { + schema.columns.forEach((column, index) => { + if (!column.name) { + errors.push({ + path: `${path}.columns[${index}]`, + message: 'Column requires name property', + type: 'error', + code: 'MISSING_COLUMN_NAME' + }); + } + }); + } + // Validate fields if present + if (schema.fields && Array.isArray(schema.fields)) { + schema.fields.forEach((field, index) => { + if (!field.name) { + errors.push({ + path: `${path}.fields[${index}]`, + message: 'Field requires name property', + type: 'error', + code: 'MISSING_FIELD_NAME' + }); + } + }); + } + } + return errors; +} +/** + * Validate form schema specific properties + */ +function validateFormSchema(schema, path = 'schema') { + const errors = []; + if (schema.type === 'form') { + if (schema.fields && Array.isArray(schema.fields)) { + schema.fields.forEach((field, index) => { + if (!field.name) { + errors.push({ + path: `${path}.fields[${index}]`, + message: 'Form field requires name property', + type: 'error', + code: 'MISSING_FIELD_NAME' + }); + } + // Check for duplicate field names + const duplicates = schema.fields.filter((f) => f.name === field.name); + if (duplicates.length > 1) { + errors.push({ + path: `${path}.fields[${index}]`, + message: `Duplicate field name: ${field.name}`, + type: 'warning', + code: 'DUPLICATE_FIELD_NAME' + }); + } + }); + } + } + return errors; +} +/** + * Validate child schemas recursively + */ +function validateChildren(schema, path = 'schema') { + const errors = []; + const children = schema.children || schema.body; + if (children) { + if (Array.isArray(children)) { + children.forEach((child, index) => { + if (typeof child === 'object' && child !== null) { + const childResult = validateSchema(child, `${path}.children[${index}]`); + errors.push(...childResult.errors, ...childResult.warnings); + } + }); + } + else if (typeof children === 'object' && children !== null) { + const childResult = validateSchema(children, `${path}.children`); + errors.push(...childResult.errors, ...childResult.warnings); + } + } + return errors; +} +/** + * Validate a complete schema + * + * @param schema - The schema to validate + * @param options - Validation options + * @returns Validation result with errors and warnings + * + * @example + * ```typescript + * const result = validateSchema({ + * type: 'form', + * fields: [ + * { name: 'email', type: 'input' } + * ] + * }); + * + * if (!result.valid) { + * console.error('Validation errors:', result.errors); + * } + * ``` + */ +export function validateSchema(schema, path = 'schema') { + const allErrors = []; + // Validate base schema + allErrors.push(...validateBaseSchema(schema, path)); + // Validate type-specific schemas + allErrors.push(...validateCRUDSchema(schema, path)); + allErrors.push(...validateFormSchema(schema, path)); + // Validate children recursively + allErrors.push(...validateChildren(schema, path)); + const errors = allErrors.filter(e => e.type === 'error'); + const warnings = allErrors.filter(e => e.type === 'warning'); + return { + valid: errors.length === 0, + errors, + warnings + }; +} +/** + * Assert that a schema is valid, throwing an error if not + * + * @param schema - The schema to validate + * @throws Error if schema is invalid + * + * @example + * ```typescript + * try { + * assertValidSchema(schema); + * // Schema is valid, continue rendering + * } catch (error) { + * console.error('Invalid schema:', error.message); + * } + * ``` + */ +export function assertValidSchema(schema) { + const result = validateSchema(schema); + if (!result.valid) { + const errorMessages = result.errors.map(e => `${e.path}: ${e.message}`).join('\n'); + throw new Error(`Schema validation failed:\n${errorMessages}`); + } +} +/** + * Check if a value is a valid schema + * + * @param value - The value to check + * @returns True if the value is a valid schema + * + * @example + * ```typescript + * if (isValidSchema(data)) { + * renderSchema(data); + * } + * ``` + */ +export function isValidSchema(value) { + const result = validateSchema(value); + return result.valid; +} +/** + * Get a human-readable error summary + * + * @param result - The validation result + * @returns Formatted error summary + */ +export function formatValidationErrors(result) { + const parts = []; + if (result.errors.length > 0) { + parts.push('Errors:'); + result.errors.forEach(error => { + parts.push(` - ${error.path}: ${error.message}`); + }); + } + if (result.warnings.length > 0) { + parts.push('Warnings:'); + result.warnings.forEach(warning => { + parts.push(` - ${warning.path}: ${warning.message}`); + }); + } + return parts.join('\n'); +} diff --git a/packages/core/src/validation/schema-validator.ts b/packages/core/src/validation/schema-validator.ts new file mode 100644 index 000000000..36af90a2d --- /dev/null +++ b/packages/core/src/validation/schema-validator.ts @@ -0,0 +1,336 @@ +/** + * @object-ui/core - Schema Validation + * + * Runtime validation utilities for Object UI schemas. + * These utilities help ensure schemas are valid before rendering. + * + * @module validation + * @packageDocumentation + */ + +import type { BaseSchema } from '@object-ui/types'; + +/** + * Validation error details + */ +export interface ValidationError { + path: string; + message: string; + type: 'error' | 'warning'; + code?: string; +} + +/** + * Validation result + */ +export interface ValidationResult { + valid: boolean; + errors: ValidationError[]; + warnings: ValidationError[]; +} + +/** + * Validation rules for base schema + */ +const BASE_SCHEMA_RULES = { + type: { + required: true, + validate: (value: any) => typeof value === 'string' && value.length > 0, + message: 'type must be a non-empty string' + }, + id: { + required: false, + validate: (value: any) => typeof value === 'string', + message: 'id must be a string' + }, + className: { + required: false, + validate: (value: any) => typeof value === 'string', + message: 'className must be a string' + }, + visible: { + required: false, + validate: (value: any) => typeof value === 'boolean', + message: 'visible must be a boolean' + }, + disabled: { + required: false, + validate: (value: any) => typeof value === 'boolean', + message: 'disabled must be a boolean' + } +}; + +/** + * Validate a schema against base rules + */ +function validateBaseSchema(schema: any, path: string = 'schema'): ValidationError[] { + const errors: ValidationError[] = []; + + if (!schema || typeof schema !== 'object') { + errors.push({ + path, + message: 'Schema must be an object', + type: 'error', + code: 'INVALID_SCHEMA' + }); + return errors; + } + + // Validate required and optional properties + Object.entries(BASE_SCHEMA_RULES).forEach(([key, rule]) => { + const value = schema[key]; + + if (rule.required && value === undefined) { + errors.push({ + path: `${path}.${key}`, + message: `${key} is required`, + type: 'error', + code: 'MISSING_REQUIRED' + }); + } + + if (value !== undefined && !rule.validate(value)) { + errors.push({ + path: `${path}.${key}`, + message: rule.message, + type: 'error', + code: 'INVALID_TYPE' + }); + } + }); + + return errors; +} + +/** + * Validate CRUD schema specific properties + */ +function validateCRUDSchema(schema: any, path: string = 'schema'): ValidationError[] { + const errors: ValidationError[] = []; + + if (schema.type === 'crud') { + // Check required properties for CRUD + if (!schema.columns || !Array.isArray(schema.columns)) { + errors.push({ + path: `${path}.columns`, + message: 'CRUD schema requires columns array', + type: 'error', + code: 'MISSING_COLUMNS' + }); + } + + if (!schema.api && !schema.dataSource) { + errors.push({ + path: `${path}.api`, + message: 'CRUD schema requires api or dataSource', + type: 'warning', + code: 'MISSING_DATA_SOURCE' + }); + } + + // Validate columns + if (schema.columns && Array.isArray(schema.columns)) { + schema.columns.forEach((column: any, index: number) => { + if (!column.name) { + errors.push({ + path: `${path}.columns[${index}]`, + message: 'Column requires name property', + type: 'error', + code: 'MISSING_COLUMN_NAME' + }); + } + }); + } + + // Validate fields if present + if (schema.fields && Array.isArray(schema.fields)) { + schema.fields.forEach((field: any, index: number) => { + if (!field.name) { + errors.push({ + path: `${path}.fields[${index}]`, + message: 'Field requires name property', + type: 'error', + code: 'MISSING_FIELD_NAME' + }); + } + }); + } + } + + return errors; +} + +/** + * Validate form schema specific properties + */ +function validateFormSchema(schema: any, path: string = 'schema'): ValidationError[] { + const errors: ValidationError[] = []; + + if (schema.type === 'form') { + if (schema.fields && Array.isArray(schema.fields)) { + schema.fields.forEach((field: any, index: number) => { + if (!field.name) { + errors.push({ + path: `${path}.fields[${index}]`, + message: 'Form field requires name property', + type: 'error', + code: 'MISSING_FIELD_NAME' + }); + } + + // Check for duplicate field names + const duplicates = schema.fields.filter((f: any) => f.name === field.name); + if (duplicates.length > 1) { + errors.push({ + path: `${path}.fields[${index}]`, + message: `Duplicate field name: ${field.name}`, + type: 'warning', + code: 'DUPLICATE_FIELD_NAME' + }); + } + }); + } + } + + return errors; +} + +/** + * Validate child schemas recursively + */ +function validateChildren(schema: any, path: string = 'schema'): ValidationError[] { + const errors: ValidationError[] = []; + + const children = schema.children || schema.body; + if (children) { + if (Array.isArray(children)) { + children.forEach((child: any, index: number) => { + if (typeof child === 'object' && child !== null) { + const childResult = validateSchema(child, `${path}.children[${index}]`); + errors.push(...childResult.errors, ...childResult.warnings); + } + }); + } else if (typeof children === 'object' && children !== null) { + const childResult = validateSchema(children, `${path}.children`); + errors.push(...childResult.errors, ...childResult.warnings); + } + } + + return errors; +} + +/** + * Validate a complete schema + * + * @param schema - The schema to validate + * @param options - Validation options + * @returns Validation result with errors and warnings + * + * @example + * ```typescript + * const result = validateSchema({ + * type: 'form', + * fields: [ + * { name: 'email', type: 'input' } + * ] + * }); + * + * if (!result.valid) { + * console.error('Validation errors:', result.errors); + * } + * ``` + */ +export function validateSchema( + schema: any, + path: string = 'schema' +): ValidationResult { + const allErrors: ValidationError[] = []; + + // Validate base schema + allErrors.push(...validateBaseSchema(schema, path)); + + // Validate type-specific schemas + allErrors.push(...validateCRUDSchema(schema, path)); + allErrors.push(...validateFormSchema(schema, path)); + + // Validate children recursively + allErrors.push(...validateChildren(schema, path)); + + const errors = allErrors.filter(e => e.type === 'error'); + const warnings = allErrors.filter(e => e.type === 'warning'); + + return { + valid: errors.length === 0, + errors, + warnings + }; +} + +/** + * Assert that a schema is valid, throwing an error if not + * + * @param schema - The schema to validate + * @throws Error if schema is invalid + * + * @example + * ```typescript + * try { + * assertValidSchema(schema); + * // Schema is valid, continue rendering + * } catch (error) { + * console.error('Invalid schema:', error.message); + * } + * ``` + */ +export function assertValidSchema(schema: any): asserts schema is BaseSchema { + const result = validateSchema(schema); + + if (!result.valid) { + const errorMessages = result.errors.map(e => `${e.path}: ${e.message}`).join('\n'); + throw new Error(`Schema validation failed:\n${errorMessages}`); + } +} + +/** + * Check if a value is a valid schema + * + * @param value - The value to check + * @returns True if the value is a valid schema + * + * @example + * ```typescript + * if (isValidSchema(data)) { + * renderSchema(data); + * } + * ``` + */ +export function isValidSchema(value: any): value is BaseSchema { + const result = validateSchema(value); + return result.valid; +} + +/** + * Get a human-readable error summary + * + * @param result - The validation result + * @returns Formatted error summary + */ +export function formatValidationErrors(result: ValidationResult): string { + const parts: string[] = []; + + if (result.errors.length > 0) { + parts.push('Errors:'); + result.errors.forEach(error => { + parts.push(` - ${error.path}: ${error.message}`); + }); + } + + if (result.warnings.length > 0) { + parts.push('Warnings:'); + result.warnings.forEach(warning => { + parts.push(` - ${warning.path}: ${warning.message}`); + }); + } + + return parts.join('\n'); +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index b596ae672..1081aa700 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -4,7 +4,12 @@ "outDir": "dist", "rootDir": "src", "noEmit": false, - "declaration": true + "declaration": true, + "composite": true }, - "include": ["src"] + "include": ["src"], + "exclude": ["src/**/*.test.ts"], + "references": [ + { "path": "../types" } + ] } diff --git a/packages/data-objectql/tsconfig.json b/packages/data-objectql/tsconfig.json index 516a2824e..48103b0db 100644 --- a/packages/data-objectql/tsconfig.json +++ b/packages/data-objectql/tsconfig.json @@ -4,8 +4,13 @@ "outDir": "./dist", "rootDir": "./src", "declaration": true, - "declarationDir": "./dist" + "declarationDir": "./dist", + "composite": true, + "noEmit": false }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"], + "references": [ + { "path": "../types" } + ] } diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 24a7ca4af..19bd07b2c 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -5,7 +5,12 @@ "rootDir": "src", "jsx": "react-jsx", "noEmit": false, - "declaration": true + "declaration": true, + "composite": true }, - "include": ["src"] + "include": ["src"], + "references": [ + { "path": "../types" }, + { "path": "../core" } + ] } diff --git a/packages/types/.gitignore b/packages/types/.gitignore index 1eae0cf67..8ace31571 100644 --- a/packages/types/.gitignore +++ b/packages/types/.gitignore @@ -1,2 +1,7 @@ dist/ node_modules/ +# Ignore generated files in src (should only be in dist) +src/**/*.d.ts +src/**/*.js +!src/**/*.test.ts +!src/**/*.test.js diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts new file mode 100644 index 000000000..4e27964fd --- /dev/null +++ b/packages/types/src/api.ts @@ -0,0 +1,464 @@ +/** + * @object-ui/types - API and Event Schemas + * + * Type definitions for API integration and event handling. + * These schemas enable dynamic API calls and event-driven interactions. + * + * @module api + * @packageDocumentation + */ + +import type { BaseSchema } from './base'; + +/** + * HTTP Method types + */ +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'; + +/** + * API request configuration + */ +export interface APIRequest { + /** + * API endpoint URL + * Supports variable substitution: "/api/users/${userId}" + */ + url: string; + /** + * HTTP method + * @default 'GET' + */ + method?: HTTPMethod; + /** + * Request headers + */ + headers?: Record; + /** + * Request body data + * For POST, PUT, PATCH requests + */ + data?: any; + /** + * Query parameters + */ + params?: Record; + /** + * Request timeout in milliseconds + */ + timeout?: number; + /** + * Whether to send credentials (cookies) + * @default false + */ + withCredentials?: boolean; + /** + * Data transformation function + * Transform request data before sending + */ + transformRequest?: string; + /** + * Response transformation function + * Transform response data after receiving + */ + transformResponse?: string; +} + +/** + * API configuration for components + */ +export interface APIConfig { + /** + * API request configuration + */ + request?: APIRequest; + /** + * Success handler + * JavaScript expression or function name + */ + onSuccess?: string; + /** + * Error handler + * JavaScript expression or function name + */ + onError?: string; + /** + * Loading indicator + * Whether to show loading state during request + * @default true + */ + showLoading?: boolean; + /** + * Success message to display + */ + successMessage?: string; + /** + * Error message to display + */ + errorMessage?: string; + /** + * Whether to reload data after success + * @default false + */ + reload?: boolean; + /** + * Whether to redirect after success + */ + redirect?: string; + /** + * Whether to close dialog/modal after success + * @default false + */ + close?: boolean; + /** + * Retry configuration + */ + retry?: { + /** + * Maximum retry attempts + */ + maxAttempts?: number; + /** + * Delay between retries in milliseconds + */ + delay?: number; + /** + * HTTP status codes to retry + */ + retryOn?: number[]; + }; + /** + * Cache configuration + */ + cache?: { + /** + * Cache key + */ + key?: string; + /** + * Cache duration in milliseconds + */ + duration?: number; + /** + * Whether to use stale cache while revalidating + */ + staleWhileRevalidate?: boolean; + }; +} + +/** + * Event handler configuration + */ +export interface EventHandler { + /** + * Event type + */ + event: string; + /** + * Handler type + */ + type: 'action' | 'api' | 'script' | 'navigation' | 'dialog' | 'toast' | 'custom'; + /** + * Action configuration (for type: 'action') + */ + action?: { + /** + * Action name/identifier + */ + name: string; + /** + * Action parameters + */ + params?: Record; + }; + /** + * API configuration (for type: 'api') + */ + api?: APIConfig; + /** + * Script to execute (for type: 'script') + * JavaScript code as string + */ + script?: string; + /** + * Navigation target (for type: 'navigation') + */ + navigate?: { + /** + * Target URL or route + */ + to: string; + /** + * Navigation type + */ + type?: 'push' | 'replace' | 'reload'; + /** + * Query parameters + */ + params?: Record; + /** + * Open in new window/tab + */ + external?: boolean; + }; + /** + * Dialog configuration (for type: 'dialog') + */ + dialog?: { + /** + * Dialog type + */ + type: 'alert' | 'confirm' | 'prompt' | 'modal'; + /** + * Dialog title + */ + title?: string; + /** + * Dialog content + */ + content?: string | BaseSchema; + /** + * Dialog actions + */ + actions?: Array<{ + label: string; + handler?: EventHandler; + }>; + }; + /** + * Toast configuration (for type: 'toast') + */ + toast?: { + /** + * Toast type + */ + type: 'success' | 'error' | 'warning' | 'info'; + /** + * Toast message + */ + message: string; + /** + * Toast duration in milliseconds + */ + duration?: number; + }; + /** + * Condition for executing handler + * JavaScript expression + */ + condition?: string; + /** + * Whether to prevent default event behavior + */ + preventDefault?: boolean; + /** + * Whether to stop event propagation + */ + stopPropagation?: boolean; + /** + * Debounce delay in milliseconds + */ + debounce?: number; + /** + * Throttle delay in milliseconds + */ + throttle?: number; +} + +/** + * Component with event handlers + */ +export interface EventableSchema extends BaseSchema { + /** + * Event handlers configuration + */ + events?: EventHandler[]; + /** + * Click handler + */ + onClick?: EventHandler | string; + /** + * Change handler + */ + onChange?: EventHandler | string; + /** + * Submit handler + */ + onSubmit?: EventHandler | string; + /** + * Focus handler + */ + onFocus?: EventHandler | string; + /** + * Blur handler + */ + onBlur?: EventHandler | string; + /** + * Mouse enter handler + */ + onMouseEnter?: EventHandler | string; + /** + * Mouse leave handler + */ + onMouseLeave?: EventHandler | string; + /** + * Key down handler + */ + onKeyDown?: EventHandler | string; + /** + * Key up handler + */ + onKeyUp?: EventHandler | string; +} + +/** + * Data fetching configuration + */ +export interface DataFetchConfig { + /** + * Data source API + */ + api: string | APIRequest; + /** + * Whether to fetch on mount + * @default true + */ + fetchOnMount?: boolean; + /** + * Polling interval in milliseconds + * If set, data will be refetched at this interval + */ + pollInterval?: number; + /** + * Dependencies for refetching + * Array of variable names to watch + */ + dependencies?: string[]; + /** + * Default data before fetch completes + */ + defaultData?: any; + /** + * Transform function for fetched data + * JavaScript expression or function name + */ + transform?: string; + /** + * Filter function for data + * JavaScript expression or function name + */ + filter?: string; + /** + * Sort configuration + */ + sort?: { + /** + * Field to sort by + */ + field: string; + /** + * Sort order + */ + order: 'asc' | 'desc'; + }; + /** + * Pagination configuration + */ + pagination?: { + /** + * Current page + */ + page?: number; + /** + * Page size + */ + pageSize?: number; + /** + * Whether pagination is enabled + */ + enabled?: boolean; + }; +} + +/** + * Component with data fetching + */ +export interface DataFetchableSchema extends BaseSchema { + /** + * Data fetching configuration + */ + dataSource?: DataFetchConfig; + /** + * Loading state + */ + loading?: boolean; + /** + * Error state + */ + error?: string | null; + /** + * Fetched data + */ + data?: any; +} + +/** + * Expression evaluation context + */ +export interface ExpressionContext { + /** + * Current component data + */ + data?: any; + /** + * Global application state + */ + state?: any; + /** + * Form values (when in form context) + */ + form?: any; + /** + * Current user information + */ + user?: any; + /** + * Environment variables + */ + env?: Record; + /** + * Utility functions + */ + utils?: Record any>; +} + +/** + * Expression schema for dynamic values + */ +export interface ExpressionSchema { + /** + * Expression type + */ + type: 'expression'; + /** + * Expression string + * Supports ${} syntax for variable interpolation + */ + value: string; + /** + * Default value if expression fails + */ + defaultValue?: any; + /** + * Whether to watch and re-evaluate on context changes + * @default true + */ + reactive?: boolean; +} + +/** + * Union type of all API schemas + */ +export type APISchema = + | EventableSchema + | DataFetchableSchema + | ExpressionSchema; diff --git a/packages/types/src/base.ts b/packages/types/src/base.ts index c0b48cf6b..20be91e67 100644 --- a/packages/types/src/base.ts +++ b/packages/types/src/base.ts @@ -35,6 +35,30 @@ export interface BaseSchema { */ id?: string; + /** + * Human-readable name for the component. + * Used for form field names, labels, and debugging. + */ + name?: string; + + /** + * Display label for the component. + * Often used in forms, cards, and other UI elements. + */ + label?: string; + + /** + * Descriptive text providing additional context. + * Typically rendered as help text below the component. + */ + description?: string; + + /** + * Placeholder text for input components. + * Provides hints about expected input format or content. + */ + placeholder?: string; + /** * Tailwind CSS classes to apply to the component. * This is the primary styling mechanism in Object UI. @@ -42,6 +66,13 @@ export interface BaseSchema { */ className?: string; + /** + * Inline CSS styles as a JavaScript object. + * Use sparingly - prefer className with Tailwind. + * @example { backgroundColor: '#fff', padding: '16px' } + */ + style?: Record; + /** * Arbitrary data attached to the component. * Can be used for custom properties, state, or context. @@ -60,6 +91,58 @@ export interface BaseSchema { */ children?: SchemaNode | SchemaNode[]; + /** + * Controls whether the component is visible. + * When false, component is not rendered (display: none). + * @default true + */ + visible?: boolean; + + /** + * Expression for conditional visibility. + * Evaluated against the current data context. + * @example "${data.role === 'admin'}" + */ + visibleOn?: string; + + /** + * Controls whether the component is hidden (but still rendered). + * When true, component is rendered but not visible (visibility: hidden). + * @default false + */ + hidden?: boolean; + + /** + * Expression for conditional hiding. + * @example "${!data.isActive}" + */ + hiddenOn?: string; + + /** + * Controls whether the component is disabled. + * Applies to interactive components like buttons and inputs. + * @default false + */ + disabled?: boolean; + + /** + * Expression for conditional disabling. + * @example "${data.status === 'locked'}" + */ + disabledOn?: string; + + /** + * Test ID for automated testing. + * Rendered as data-testid attribute. + */ + testId?: string; + + /** + * Accessibility label for screen readers. + * Rendered as aria-label attribute. + */ + ariaLabel?: string; + /** * Additional properties specific to the component type. * This index signature allows type-specific extensions. diff --git a/packages/types/src/crud.ts b/packages/types/src/crud.ts new file mode 100644 index 000000000..8da971f48 --- /dev/null +++ b/packages/types/src/crud.ts @@ -0,0 +1,467 @@ +/** + * @object-ui/types - CRUD Component Schemas + * + * Type definitions for Create, Read, Update, Delete operations. + * These schemas enable building complete data management interfaces. + * + * @module crud + * @packageDocumentation + */ + +import type { BaseSchema, SchemaNode } from './base'; +import type { FormField } from './form'; +import type { TableColumn } from './data-display'; + +/** + * Action button configuration for CRUD operations + */ +export interface ActionSchema extends BaseSchema { + type: 'action'; + /** + * Action label + */ + label: string; + /** + * Action type/level + * @default 'default' + */ + level?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'default'; + /** + * Icon to display (lucide-react icon name) + */ + icon?: string; + /** + * Action variant + */ + variant?: 'default' | 'outline' | 'ghost' | 'link'; + /** + * Whether action is disabled + */ + disabled?: boolean; + /** + * Action type + */ + actionType?: 'button' | 'link' | 'dropdown'; + /** + * API endpoint to call + */ + api?: string; + /** + * HTTP method + * @default 'POST' + */ + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + /** + * Confirmation message before execution + */ + confirmText?: string; + /** + * Success message after execution + */ + successMessage?: string; + /** + * Whether to reload data after action + * @default true + */ + reload?: boolean; + /** + * Whether to close dialog/modal after action + * @default true + */ + close?: boolean; + /** + * Custom click handler + */ + onClick?: () => void | Promise; + /** + * Redirect URL after success + */ + redirect?: string; +} + +/** + * CRUD operation configuration + */ +export interface CRUDOperation { + /** + * Operation type + */ + type: 'create' | 'read' | 'update' | 'delete' | 'export' | 'import' | 'custom'; + /** + * Operation label + */ + label?: string; + /** + * Operation icon + */ + icon?: string; + /** + * Whether operation is enabled + * @default true + */ + enabled?: boolean; + /** + * API endpoint for this operation + */ + api?: string; + /** + * HTTP method + */ + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + /** + * Confirmation message + */ + confirmText?: string; + /** + * Success message + */ + successMessage?: string; + /** + * Visibility condition + */ + visibleOn?: string; + /** + * Disabled condition + */ + disabledOn?: string; +} + +/** + * Filter configuration for CRUD components + */ +export interface CRUDFilter { + /** + * Filter name (field name) + */ + name: string; + /** + * Filter label + */ + label?: string; + /** + * Filter type + */ + type?: 'input' | 'select' | 'date-picker' | 'date-range' | 'number-range'; + /** + * Filter operator + * @default 'equals' + */ + operator?: 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'gt' | 'gte' | 'lt' | 'lte' | 'between' | 'in'; + /** + * Options for select filter + */ + options?: Array<{ label: string; value: string | number }>; + /** + * Placeholder text + */ + placeholder?: string; + /** + * Default value + */ + defaultValue?: any; +} + +/** + * Toolbar configuration for CRUD components + */ +export interface CRUDToolbar { + /** + * Show create button + * @default true + */ + showCreate?: boolean; + /** + * Show refresh button + * @default true + */ + showRefresh?: boolean; + /** + * Show export button + * @default false + */ + showExport?: boolean; + /** + * Show import button + * @default false + */ + showImport?: boolean; + /** + * Show filter toggle + * @default true + */ + showFilter?: boolean; + /** + * Show search box + * @default true + */ + showSearch?: boolean; + /** + * Custom actions + */ + actions?: ActionSchema[]; +} + +/** + * CRUD pagination configuration + */ +export interface CRUDPagination { + /** + * Whether pagination is enabled + * @default true + */ + enabled?: boolean; + /** + * Default page size + * @default 10 + */ + pageSize?: number; + /** + * Page size options + * @default [10, 20, 50, 100] + */ + pageSizeOptions?: number[]; + /** + * Show total count + * @default true + */ + showTotal?: boolean; + /** + * Show page size selector + * @default true + */ + showSizeChanger?: boolean; +} + +/** + * Complete CRUD component + * Provides full Create, Read, Update, Delete functionality + */ +export interface CRUDSchema extends BaseSchema { + type: 'crud'; + /** + * CRUD title + */ + title?: string; + /** + * Resource name (singular) + * @example 'user', 'product', 'order' + */ + resource?: string; + /** + * API endpoint for list/search + */ + api?: string; + /** + * Table columns configuration + */ + columns: TableColumn[]; + /** + * Form fields for create/edit + */ + fields?: FormField[]; + /** + * Enabled operations + */ + operations?: { + create?: boolean | CRUDOperation; + read?: boolean | CRUDOperation; + update?: boolean | CRUDOperation; + delete?: boolean | CRUDOperation; + export?: boolean | CRUDOperation; + import?: boolean | CRUDOperation; + [key: string]: boolean | CRUDOperation | undefined; + }; + /** + * Toolbar configuration + */ + toolbar?: CRUDToolbar; + /** + * Filter configuration + */ + filters?: CRUDFilter[]; + /** + * Pagination configuration + */ + pagination?: CRUDPagination; + /** + * Default sort field + */ + defaultSort?: string; + /** + * Default sort order + * @default 'asc' + */ + defaultSortOrder?: 'asc' | 'desc'; + /** + * Row selection mode + */ + selectable?: boolean | 'single' | 'multiple'; + /** + * Batch actions for selected rows + */ + batchActions?: ActionSchema[]; + /** + * Row actions (displayed in each row) + */ + rowActions?: ActionSchema[]; + /** + * Custom empty state + */ + emptyState?: SchemaNode; + /** + * Whether to show loading state + * @default true + */ + loading?: boolean; + /** + * Custom loading component + */ + loadingComponent?: SchemaNode; + /** + * Table layout mode + * @default 'table' + */ + mode?: 'table' | 'grid' | 'list' | 'kanban'; + /** + * Grid columns (for grid mode) + * @default 3 + */ + gridColumns?: number; + /** + * Card template (for grid/list mode) + */ + cardTemplate?: SchemaNode; + /** + * Kanban columns (for kanban mode) + */ + kanbanColumns?: Array<{ + id: string; + title: string; + color?: string; + }>; + /** + * Kanban group field + */ + kanbanGroupField?: string; +} + +/** + * Detail view component + * Displays detailed information about a single record + */ +export interface DetailSchema extends BaseSchema { + type: 'detail'; + /** + * Detail title + */ + title?: string; + /** + * API endpoint to fetch detail data + */ + api?: string; + /** + * Resource ID to display + */ + resourceId?: string | number; + /** + * Field groups for organized display + */ + groups?: Array<{ + title?: string; + description?: string; + fields: Array<{ + name: string; + label?: string; + type?: 'text' | 'image' | 'link' | 'badge' | 'date' | 'datetime' | 'json' | 'html' | 'custom'; + format?: string; + render?: SchemaNode; + }>; + }>; + /** + * Actions available in detail view + */ + actions?: ActionSchema[]; + /** + * Tabs for additional content + */ + tabs?: Array<{ + key: string; + label: string; + content: SchemaNode | SchemaNode[]; + }>; + /** + * Show back button + * @default true + */ + showBack?: boolean; + /** + * Custom back action + */ + onBack?: () => void; + /** + * Whether to show loading state + * @default true + */ + loading?: boolean; +} + +/** + * CRUD Dialog/Modal component for CRUD operations + * Note: For general dialog usage, use DialogSchema from overlay module + */ +export interface CRUDDialogSchema extends BaseSchema { + type: 'crud-dialog'; + /** + * Dialog title + */ + title?: string; + /** + * Dialog description + */ + description?: string; + /** + * Dialog content + */ + content?: SchemaNode | SchemaNode[]; + /** + * Dialog size + * @default 'default' + */ + size?: 'sm' | 'default' | 'lg' | 'xl' | 'full'; + /** + * Dialog actions/buttons + */ + actions?: ActionSchema[]; + /** + * Whether dialog is open + */ + open?: boolean; + /** + * Close handler + */ + onClose?: () => void; + /** + * Whether clicking outside closes dialog + * @default true + */ + closeOnOutsideClick?: boolean; + /** + * Whether pressing Escape closes dialog + * @default true + */ + closeOnEscape?: boolean; + /** + * Show close button + * @default true + */ + showClose?: boolean; +} + +/** + * Union type of all CRUD schemas + */ +export type CRUDComponentSchema = + | ActionSchema + | CRUDSchema + | DetailSchema + | CRUDDialogSchema; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 34221c441..6306d2318 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -216,6 +216,37 @@ export type { APIError, } from './data'; +// ============================================================================ +// CRUD Components - Create, Read, Update, Delete Operations +// ============================================================================ +export type { + ActionSchema, + CRUDOperation, + CRUDFilter, + CRUDToolbar, + CRUDPagination, + CRUDSchema, + DetailSchema, + CRUDDialogSchema, + CRUDComponentSchema, +} from './crud'; + +// ============================================================================ +// API and Events - API Integration and Event Handling +// ============================================================================ +export type { + HTTPMethod, + APIRequest, + APIConfig, + EventHandler, + EventableSchema, + DataFetchConfig, + DataFetchableSchema, + ExpressionContext, + ExpressionSchema, + APISchema, +} from './api'; + // ============================================================================ // Union Types - Discriminated Unions for All Schemas // ============================================================================ @@ -229,6 +260,7 @@ import type { DisclosureSchema } from './disclosure'; import type { OverlaySchema } from './overlay'; import type { NavigationSchema } from './navigation'; import type { ComplexSchema } from './complex'; +import type { CRUDComponentSchema } from './crud'; /** * Union of all component schemas. @@ -243,7 +275,8 @@ export type AnySchema = | DisclosureSchema | OverlaySchema | NavigationSchema - | ComplexSchema; + | ComplexSchema + | CRUDComponentSchema; /** * Utility type to extract the schema type from a type string. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d298982ba..f30fe179c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -475,6 +475,9 @@ importers: packages/core: dependencies: + '@object-ui/types': + specifier: workspace:* + version: link:../types lodash: specifier: ^4.17.21 version: 4.17.21 diff --git a/tsconfig.json b/tsconfig.json index bdb2861db..9d7db4c39 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,10 @@ "baseUrl": ".", "paths": { + "@object-ui/types": ["packages/types/src"], + "@object-ui/types/*": ["packages/types/src/*"], + "@object-ui/core": ["packages/core/src"], + "@object-ui/core/*": ["packages/core/src/*"], "@object-ui/protocol": ["packages/core/src"], "@object-ui/protocol/*": ["packages/core/src/*"], "@object-ui/engine": ["packages/engine/src"],