diff --git a/.github/WORKFLOWS.md b/.github/WORKFLOWS.md index d53b775b..e02a78da 100644 --- a/.github/WORKFLOWS.md +++ b/.github/WORKFLOWS.md @@ -106,7 +106,6 @@ This document describes all GitHub Actions workflows and automation configured f **Labels Applied:** - `kernel` - Changes in `packages/kernel/**` - `server` - Changes in `packages/server/**` -- `ui` - Changes in `packages/ui/**` - `presets` - Changes in `packages/presets/**` - `documentation` - Changes in docs or markdown files - `workflows` - Changes in `.github/**` diff --git a/.github/labeler.yml b/.github/labeler.yml index 6741a6a2..9a35e52c 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -10,10 +10,6 @@ - changed-files: - any-glob-to-any-file: 'packages/server/**/*' -'ui': - - changed-files: - - any-glob-to-any-file: 'packages/ui/**/*' - 'presets': - changed-files: - any-glob-to-any-file: 'packages/presets/**/*' diff --git a/.github/labels.yml b/.github/labels.yml index b6d3faad..ff6fe212 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -11,10 +11,6 @@ labels: color: '0366d6' description: 'Changes in @objectos/server package' - - name: ui - color: '0366d6' - description: 'Changes in @objectos/ui package' - - name: presets color: '0366d6' description: 'Changes in preset packages' diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d3cc0b59..164d1bb2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -6,7 +6,7 @@ ObjectOS is a **metadata-driven runtime engine** that transforms declarative YAM ## Protocol Foundation: @objectstack/spec -ObjectOS is built on the **[@objectstack/spec](https://www.npmjs.com/package/@objectstack/spec)** protocol, which defines the "DNA" of the ObjectStack ecosystem. The spec provides: +ObjectOS is built on the **[@objectstack/spec](https://github.com/objectstack-ai/spec)** protocol, which defines the "DNA" of the ObjectStack ecosystem. The spec provides: ### 1. **Strict Type Definitions** - **Zod Schemas**: Runtime validation for configuration and data @@ -36,7 +36,7 @@ All ObjectOS plugins must conform to this lifecycle for consistency and predicta ## The Three-Repository Model ### @objectstack/spec (Protocol Definition) -- **Location**: https://www.npmjs.com/package/@objectstack/spec +- **Location**: https://github.com/objectstack-ai/spec - **Purpose**: Defines the protocol and type contracts - **Key Exports**: - `Data.*` - Object schemas, field types, queries @@ -60,7 +60,6 @@ All ObjectOS plugins must conform to this lifecycle for consistency and predicta - **Key Packages**: - `@objectos/kernel` - Core execution engine - `@objectos/server` - HTTP API layer - - `@objectos/ui` - React UI components ## Core Architectural Principle @@ -302,41 +301,9 @@ export class ObjectDataController { | DELETE | `/api/data/:object/:id` | Delete record | | GET | `/api/metadata/:object` | Get object metadata | -## Layer 5: UI Layer (@objectos/ui) +## Layer 5: UI Layer -### Component Architecture - -The UI layer provides **metadata-driven React components**: - -```typescript -// Automatically generates a data grid from metadata - - -// Automatically generates a form from metadata - -``` - -### Key Components - -1. **ObjectGrid**: Airtable-like data grid with inline editing -2. **ObjectForm**: Salesforce-like detail form with sections -3. **ObjectChart**: Chart component for analytics -4. **FilterBuilder**: Visual query builder - -### Design System - -- **Framework**: React 18+ with TypeScript -- **Styling**: Tailwind CSS -- **Components**: Shadcn/ui -- **State**: React Query for server state -- **Grid**: TanStack Table +**Note**: The UI layer has been moved to a separate project and is no longer part of this monorepo. The UI components are developed independently and can be integrated with ObjectOS through the API layer. ## Extension Points diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2145bff7..b06589b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,20 +18,21 @@ objectos/ ├── packages/ │ ├── kernel/ # @objectos/kernel - Core runtime engine │ ├── server/ # @objectos/server - NestJS HTTP server -│ ├── ui/ # @objectos/ui - React components │ └── presets/ # @objectos/preset-* - Standard metadata +├── apps/ +│ └── site/ # @objectos/site - Documentation site ├── examples/ # Example applications -├── docs/ # VitePress documentation -└── apps/ # Full-stack applications (if any) +└── docs/ # VitePress documentation ``` +**Note**: The UI package (`@objectos/ui`) has been moved to a separate repository and is developed independently. + ### Package Responsibilities | Package | Role | Can Import | Cannot Import | |---------|------|------------|---------------| | `@objectos/kernel` | Core logic, object registry, hooks | `@objectql/types`, `@objectql/core` | `pg`, `express`, `nest` | | `@objectos/server` | HTTP layer, REST API | `@objectos/kernel`, `@nestjs/*` | `knex`, direct SQL | -| `@objectos/ui` | React components | `@objectos/kernel` types | Server-specific code | | `@objectos/preset-*` | Metadata YAML files | None | No .ts files allowed | ## Development Standards diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md index 374c04da..f45c3a09 100644 --- a/QUICK_REFERENCE.md +++ b/QUICK_REFERENCE.md @@ -113,19 +113,9 @@ NestJS HTTP server. - `DELETE /api/data/:object/:id` - Delete record - `GET /api/metadata/:object` - Get metadata -### @objectos/ui +### UI Components -React UI components. - -```tsx -import { ObjectGrid, ObjectForm } from '@objectos/ui'; - -// Auto-generated data grid - - -// Auto-generated form - -``` +**Note**: The UI components have been moved to a separate project and are no longer part of this monorepo. They are developed independently and can be integrated with ObjectOS through the API layer. ## Field Types diff --git a/README.md b/README.md index e0e954c9..a6f4e368 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ ObjectOS is built as a modular Monorepo using **NestJS** and follows the **@obje ### Protocol Compliance -ObjectOS adheres to the [@objectstack/spec](https://www.npmjs.com/package/@objectstack/spec) protocol, which defines: +ObjectOS adheres to the [@objectstack/spec](https://github.com/objectstack-ai/spec) protocol, which defines: - **Kernel Protocol**: Plugin lifecycle, manifest structure, and context interfaces - **Data Protocol**: Object schemas, field types, queries, and hooks - **System Protocol**: Audit logging, events, and job scheduling diff --git a/apps/site/content/docs/guide/index.mdx b/apps/site/content/docs/guide/index.mdx index e7d895ae..612814b3 100644 --- a/apps/site/content/docs/guide/index.mdx +++ b/apps/site/content/docs/guide/index.mdx @@ -23,7 +23,6 @@ ObjectOS is a **metadata-driven runtime engine** that interprets and executes bu │ ObjectOS (Runtime Repository - This One) │ │ - @objectos/kernel: Execution engine │ │ - @objectos/server: HTTP API layer │ -│ - @objectos/ui: React components │ └─────────────────────┬───────────────────────────┘ │ ▼ @@ -304,22 +303,9 @@ curl -X POST http://localhost:3000/api/data/contacts \ }' ``` -## Using the UI Components +## Using UI Components -ObjectOS provides React components that automatically render based on metadata: - -```tsx -import { ObjectGrid, ObjectForm } from '@objectos/ui'; - -function ContactsPage() { - return ( -
-

Contacts

- {/* Automatically generates a data grid */} - -
- ); -} +> **Note**: The UI components have been moved to a separate project and are no longer part of this monorepo. They can be integrated with ObjectOS through the HTTP API layer (`@objectos/server`) which provides REST endpoints for metadata and data access. function ContactDetail({ contactId }) { return ( diff --git a/apps/site/content/docs/guide/logic-actions.mdx b/apps/site/content/docs/guide/logic-actions.mdx index 04e464c2..46b51473 100644 --- a/apps/site/content/docs/guide/logic-actions.mdx +++ b/apps/site/content/docs/guide/logic-actions.mdx @@ -466,20 +466,24 @@ const result = await kernel.executeAction('contacts.sendEmail', { ### From UI -```typescript -import { useAction } from '@objectos/ui'; +> **Note**: The UI components have been moved to a separate project. The example below shows the conceptual pattern for invoking actions from a UI application. +```typescript +// Example pattern for UI integration function ContactDetail({ contactId }) { - const sendEmail = useAction('contacts.sendEmail'); - const handleSendEmail = async () => { - const result = await sendEmail({ - id: contactId, - subject: 'Follow up', - body: 'Thank you for your interest' + const result = await fetch('/api/actions/contacts.sendEmail', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: contactId, + subject: 'Follow up', + body: 'Thank you for your interest' + }) }); - alert(result.message); + const data = await result.json(); + alert(data.message); }; return ( diff --git a/apps/site/content/docs/guide/platform-components.mdx b/apps/site/content/docs/guide/platform-components.mdx index ec30a355..c4b26dea 100644 --- a/apps/site/content/docs/guide/platform-components.mdx +++ b/apps/site/content/docs/guide/platform-components.mdx @@ -53,18 +53,9 @@ The `@objectos/server` package is the Gateway. It translates HTTP/WebSockets int ## 🖥️ 3. Interaction Support (UI Layer) -The `@objectos/ui` (Components) and `@objectos/web` (App) packages provide the human interface. +> **Note**: The UI components have been moved to a separate project and are no longer part of this monorepo. -### Component Breakdown - -| Component | Responsibility | Implementation Notes | -| :--- | :--- | :--- | -| **ObjectGrid** | Data Table with "Excel-like" features. | Uses **TanStack Table**. Implements Virtual Scroll for 100k+ rows. | -| **ObjectForm** | Dynamic Record Editor. | Uses **React-Hook-Form**. Generates Zod schema from Metadata at runtime. | -| **LayoutShell** | Application chrome (Sidebar, Header). | Responsive. Adapts menu based on user permissions. | -| **DataQueryHook** | React Query wrapper for API. | Cache management. `useQuery(['data', 'contacts'], ...)` | - -### Functional Realization: "Dynamic Types" +The UI layer provides the human interface, integrating with ObjectOS through the HTTP API exposed by `@objectos/server`. * **Design**: The UI downloads metadata initially. * **Flow**: `schema.json` received -> `FieldFactory` maps `type: 'date'` to `` -> Renders Cell. diff --git a/apps/site/content/docs/guide/ui-framework.mdx b/apps/site/content/docs/guide/ui-framework.mdx index 2bb29c44..60e6bbe5 100644 --- a/apps/site/content/docs/guide/ui-framework.mdx +++ b/apps/site/content/docs/guide/ui-framework.mdx @@ -2,7 +2,9 @@ title: Standard UI Components Reference --- -This document defines the standard component library for `@objectos/ui`. These components are the reference implementations for the **View & Layout Specifications**. +> **Note**: The UI components have been moved to a separate project and are no longer part of this monorepo. This document is kept for reference and describes the design principles for UI components that integrate with ObjectOS. + +This document defines the standard component library for UI components. These components are the reference implementations for the **View & Layout Specifications**. --- diff --git a/apps/web/index.html b/apps/web/index.html deleted file mode 100644 index bea339ad..00000000 --- a/apps/web/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - ObjectOS - - - -
- - - diff --git a/apps/web/package.json b/apps/web/package.json deleted file mode 100644 index bef1b24b..00000000 --- a/apps/web/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "@objectos/web", - "private": true, - "version": "0.1.0", - "license": "AGPL-3.0", - "type": "module", - "scripts": { - "dev": "vite", - "build:watch": "vite build --watch", - "build": "tsc -b && vite build", - "test": "echo \"No tests specified\" && exit 0", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "@objectos/ui": "workspace:*", - "@objectql/types": "^3.0.1", - "ag-grid-community": "^35.0.0", - "ag-grid-react": "^35.0.0", - "better-auth": "^1.4.10", - "clsx": "^2.1.0", - "lucide-react": "^0.344.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-hook-form": "^7.54.2", - "react-router-dom": "^7.12.0", - "tailwind-merge": "^2.2.1" - }, - "devDependencies": { - "@eslint/js": "^9.17.0", - "@tailwindcss/postcss": "^4.1.18", - "@types/react": "^19.0.11", - "@types/react-dom": "^19.0.5", - "@vitejs/plugin-react": "^4.3.4", - "eslint": "^9.17.0", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.16", - "globals": "^15.14.0", - "postcss": "^8.4.49", - "tailwindcss": "^4.1.18", - "tailwindcss-animate": "^1.0.7", - "typescript": "~5.6.2", - "typescript-eslint": "^8.18.2", - "vite": "^6.0.5" - } -} diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js deleted file mode 100644 index a7f73a2d..00000000 --- a/apps/web/postcss.config.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - plugins: { - '@tailwindcss/postcss': {}, - }, -} diff --git a/apps/web/public/logo.svg b/apps/web/public/logo.svg deleted file mode 100644 index e56ca292..00000000 --- a/apps/web/public/logo.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx deleted file mode 100644 index c5605145..00000000 --- a/apps/web/src/App.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Routes, Route, Navigate } from 'react-router-dom'; -import AppList from './pages/AppList'; -import Login from './pages/Login'; -import AppDashboard from './pages/AppDashboard'; // New App Home -import Settings from './pages/Settings'; -import Organization from './pages/Organization'; -import { AuthProvider, useAuth } from './context/AuthContext'; -import { MainLayout } from './layouts/MainLayout'; -import { WorkspaceLayout } from './layouts/WorkspaceLayout'; -import { ObjectListRoute } from './pages/objects/ObjectListRoute'; -import { ObjectDetailRoute } from './pages/objects/ObjectDetailRoute'; -import * as paths from './routes'; - -function AppContent() { - const { user, loading } = useAuth(); - - if (loading) { - return ( -
-
-
- ); - } - - // Auth Routing - if (!user) { - return ( - - } /> - } /> - - ); - } - - return ( - - } /> - - {/* Main App Selection Layout */} - }> - } /> - } /> - - - {/* Workspace/Dashboard Layout */} - }> - {/* The App Home Dashboard showing menu shortcuts */} - } /> - - {/* Object Routes */} - } /> - } /> - } /> - {/* Legacy/Compat routes support */} - } /> - - {/* Global/Standard Routes */} - } /> - } /> - - - {/* Fallback */} - } /> - - ); -} - -function App() { - return ( - - - - ); -} - -export default App; - diff --git a/apps/web/src/components/DynamicIcon.tsx b/apps/web/src/components/DynamicIcon.tsx deleted file mode 100644 index 246ab28a..00000000 --- a/apps/web/src/components/DynamicIcon.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import * as LucideIcons from 'lucide-react'; - -interface DynamicIconProps extends React.ComponentProps<'svg'> { - name?: string; - fallback?: React.ElementType; - className?: string; -} - -export function DynamicIcon({ name, fallback, className, ...props }: DynamicIconProps) { - const Fallback = fallback || LucideIcons.FileText; - - if (!name) { - return ; - } - - let iconName = name; - - // Handle Remix Icon names (ri-dashboard-line -> Dashboard) - // Common mappings if needed, or just stripping prefixes - if (name.startsWith('ri-')) { - iconName = name - .replace(/^ri-/, '') - .replace(/-line$/, '') - .replace(/-fill$/, ''); - } - - // Convert kebab-case to PascalCase (dashboard-layout -> DashboardLayout) - const pascalName = iconName - .split('-') - .map(part => part.charAt(0).toUpperCase() + part.slice(1)) - .join(''); - - // Try to find the icon in Lucide - // 1. Exact PascalCase match - // 2. Case-insensitive match (less likely needed if normalization is good) - const IconComponent = (LucideIcons as any)[pascalName] || (LucideIcons as any)[iconName]; - - if (IconComponent) { - return ; - } - - // If not found, return fallback - return ; -} diff --git a/apps/web/src/components/app-sidebar.tsx b/apps/web/src/components/app-sidebar.tsx deleted file mode 100644 index b95be45b..00000000 --- a/apps/web/src/components/app-sidebar.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import * as React from "react" -import { - Sidebar, - SidebarContent, - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarRail, - SidebarFooter, - SidebarMenuBadge, - SidebarMenuSub, - SidebarMenuSubItem, - SidebarMenuSubButton, - Collapsible, - CollapsibleTrigger, - CollapsibleContent, -} from "@objectos/ui" -import * as LucideIcons from "lucide-react" -import { useRouter } from "../hooks/useRouter" -import { NavUser } from "./nav-user" -import { useAuth } from "../context/AuthContext" -import { DynamicIcon } from "./DynamicIcon" - -export function AppSidebar({ objects, appMetadata, ...props }: React.ComponentProps & { objects: Record, appMetadata?: any }) { - const { path, navigate } = useRouter() - const { user } = useAuth() - - // Parse current app context - const parts = path.split('/'); - const currentApp = parts[1] === 'app' ? parts[2] : null; - const getObjectPath = (name: string) => currentApp ? `/app/${currentApp}/object/${name}` : `/object/${name}`; - - const rawMenu = appMetadata?.menu; - - // Robust check for grouped vs flat menu structure - // A section has 'items' but NO 'type', 'object', or 'url' - const isSection = (item: any) => item && item.items && Array.isArray(item.items) && !item.type && !item.object && !item.url; - - const isGrouped = Array.isArray(rawMenu) && rawMenu.length > 0 && isSection(rawMenu[0]); - - // Use a special key/flag for the default wrapper to hide the label later - const menuSections = rawMenu ? (isGrouped ? rawMenu : [{ label: 'Menu', items: rawMenu, _isDefaultWrapper: true }]) : []; - - const renderMenuItem = (item: any, idx: number) => { - if (item.visible === false) return null; - - // Handle Separator/Divider - if (item.type === 'divider') { - return
; - } - - // Default label if missing (e.g. for dividers context or malformed data) - const label = item.label || ''; - const itemType = item.type || 'page'; - - // Determine active state - let isActive = false; - if (itemType === 'object') { - isActive = path.includes(`/object/${item.object}`); - } else if (itemType === 'page' || itemType === 'url') { - isActive = item.url ? path.endsWith(item.url) : false; - } - - const handleClick = () => { - if (itemType === 'object' && item.object) { - navigate(getObjectPath(item.object)); - } else if (itemType === 'page' && item.url) { - navigate(item.url); - } else if (itemType === 'url' && item.url) { - window.open(item.url, '_blank'); - } - }; - - // Handle Nested Items (Submenus) - if (item.items && item.items.length > 0) { - return ( - - - - - - {label} - - - - - - {item.items.map((subItem: any, subIdx: number) => { - if (subItem.visible === false) return null; - const subItemType = subItem.type || 'page'; - return ( - - { - if (subItemType === 'object') navigate(getObjectPath(subItem.object_name)); - else if (subItemType === 'page') navigate(subItem.url); - else if (subItemType === 'url') window.open(subItem.url, '_blank'); - }} - > - {subItem.label} - {subItem.badge && {subItem.badge}} - - - ); - })} - - - - - ); - } - - return ( - - - - {label} - {item.badge && {item.badge}} - - - ); - }; - - return ( - - - - - navigate('/')}> -
- -
-
- {appMetadata?.label || 'ObjectOS'} -
-
-
-
-
- - {menuSections.map((section: any, idx: number) => { - const isCollapsible = section.collapsible === true; - const isCollapsed = section.collapsed === true; - - // Helper to render group content - const renderGroupContent = () => ( - - {section.items?.map((item: any, itemIdx: number) => renderMenuItem(item, itemIdx))} - - ); - - if (isCollapsible) { - return ( - - - - - {section.label} - - - - - - {renderGroupContent()} - - - - - ); - } - - return ( - - {!section._isDefaultWrapper && {section.label}} - - {renderGroupContent()} - - - ); - })} - - - - - -
- ) -} diff --git a/apps/web/src/components/dashboard/EnhancedObjectListView.tsx b/apps/web/src/components/dashboard/EnhancedObjectListView.tsx deleted file mode 100644 index c25f12dd..00000000 --- a/apps/web/src/components/dashboard/EnhancedObjectListView.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { ObjectGridTable } from '@objectos/ui'; -import type { ObjectConfig } from '@objectql/types'; - -/** - * Enhanced ObjectListView using ObjectGridTable - * This is an improved version that uses metadata-driven AG Grid table - */ - -interface EnhancedObjectListViewProps { - objectName: string; - user: any; -} - -export function EnhancedObjectListView({ objectName, user }: EnhancedObjectListViewProps) { - const [data, setData] = useState([]); - const [objectConfig, setObjectConfig] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const getHeaders = useCallback(() => { - const headers: Record = { 'Content-Type': 'application/json' }; - // Use the actual user ID from props instead of hard-coded value - if (user?.id || user?._id) { - headers['x-user-id'] = user.id || user._id; - } - return headers; - }, [user]); - - // Fetch object metadata - useEffect(() => { - if (!objectName) return; - - fetch(`/api/metadata/object/${objectName}`, { headers: getHeaders() }) - .then(async res => { - if (!res.ok) { - throw new Error(await res.text() || res.statusText); - } - return res.json(); - }) - .then(config => { - setObjectConfig(config); - }) - .catch(err => { - console.error('Failed to load object metadata:', err); - setError(err.message); - }); - }, [objectName, getHeaders]); - - // Fetch data - useEffect(() => { - if (!objectName) return; - - setLoading(true); - setError(null); - - fetch(`/api/data/${objectName}`, { headers: getHeaders() }) - .then(async res => { - if (!res.ok) { - const contentType = res.headers.get("content-type"); - if (contentType && contentType.indexOf("application/json") !== -1) { - const json = await res.json(); - throw new Error(json.error || "Failed to load data"); - } - throw new Error(await res.text() || res.statusText); - } - return res.json(); - }) - .then(result => { - const items = Array.isArray(result) ? result : (result.list || []); - setData(items); - }) - .catch(err => { - console.error(err); - setError(err.message); - setData([]); - }) - .finally(() => setLoading(false)); - }, [objectName, getHeaders]); - - const handleSelectionChanged = (selectedRows: any[]) => { - console.log('Selected rows:', selectedRows); - // You can add more selection handling logic here - }; - - if (error) { - return ( -
-
- {error} -
-
- ); - } - - if (!objectConfig) { - return ( -
-
Loading object metadata...
-
- ); - } - - return ( -
-
-

{objectConfig.label || objectName}

- {objectConfig.description && ( -

{objectConfig.description}

- )} -
- - {loading ? ( -
-
Loading data...
-
- ) : ( - - )} -
- ); -} diff --git a/apps/web/src/components/dashboard/ObjectDetailView.tsx b/apps/web/src/components/dashboard/ObjectDetailView.tsx deleted file mode 100644 index c3cd2e36..00000000 --- a/apps/web/src/components/dashboard/ObjectDetailView.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Spinner, ObjectForm } from '@objectos/ui'; -import { getHeaders } from '../../lib/api'; -import { ChevronLeft, Pencil, Trash } from 'lucide-react'; - -interface ObjectDetailViewProps { - objectName: string; - recordId: string; - navigate: (path: string) => void; - objectSchema: any; -} - -export function ObjectDetailView({ objectName, recordId, navigate, objectSchema }: ObjectDetailViewProps) { - const [data, setData] = useState(null); - const [schema, setSchema] = useState(null); - const [isEditing, setIsEditing] = useState(false); - const [loading, setLoading] = useState(true); - - const label = objectSchema?.label || objectSchema?.title || objectName; - - useEffect(() => { - setLoading(true); - Promise.all([ - fetch(`/api/data/${objectName}/${recordId}`, { headers: getHeaders() }).then(async r => { - if (!r.ok) throw new Error("Failed to load record"); - return r.json(); - }), - fetch(`/api/metadata/object/${objectName}`, { headers: getHeaders() }).then(r => r.json()) - ]).then(([record, schemaData]) => { - setData(record); - setSchema(schemaData); - }).catch(console.error) - .finally(() => setLoading(false)); - }, [objectName, recordId]); - - const handleDelete = () => { - if (!confirm('Are you sure you want to delete this record?')) return; - fetch(`/api/data/${objectName}/${recordId}`, { - method: 'DELETE', - headers: getHeaders() - }).then(() => navigate(`/object/${objectName}`)) - .catch(e => alert(e.message)); - }; - - const handleUpdate = (formData: any) => { - fetch(`/api/data/${objectName}/${recordId}`, { - method: 'PUT', - headers: getHeaders(), - body: JSON.stringify(formData) - }).then(async res => { - if(!res.ok) throw new Error(await res.text()); - return res.json(); - }).then(() => { - setIsEditing(false); - // Reload data - fetch(`/api/data/${objectName}/${recordId}`, { headers: getHeaders() }) - .then(r => r.json()) - .then(setData); - }).catch(e => alert(e.message)); - }; - - if (loading) return ( -
- -
- ); - - if (!data) return
Record not found
; - - return ( -
- {/* Header */} -
-
- -
-
- {label} - / - {recordId} -
-

{data.name || data.title || recordId}

-
-
- -
- - -
-
- - {/* Content */} -
-
- {Object.entries(data).map(([key, value]) => { - if (['id', '_id', '__v'].includes(key)) return null; - const fieldLabel = schema?.fields?.[key]?.label || key; - - return ( -
-
{fieldLabel}
-
- {typeof value === 'object' ? JSON.stringify(value) : String(value)} -
-
- ) - })} -
-
- - - - - Edit {label} - - {schema && ( - setIsEditing(false)} - /> - )} - - -
- ); -} diff --git a/apps/web/src/components/dashboard/ObjectListView.tsx b/apps/web/src/components/dashboard/ObjectListView.tsx deleted file mode 100644 index 0ee797ee..00000000 --- a/apps/web/src/components/dashboard/ObjectListView.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { - Button, - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - ObjectGridTable, - Input, - ObjectForm -} from '@objectos/ui'; -import { Plus, RefreshCw, Search } from 'lucide-react'; - -interface ObjectListViewProps { - objectName: string; - user: any; - isCreating: boolean; - navigate: (path: string) => void; - objectSchema: any; -} - -export function ObjectListView({ objectName, user, isCreating, navigate, objectSchema }: ObjectListViewProps) { - const [data, setData] = useState([]); - const [loading, setLoading] = useState(false); - const [searchTerm, setSearchTerm] = useState(''); - - // Use schema label or title or object name - const label = objectSchema?.label || objectSchema?.title || objectName; - - // Headers helper - const getHeaders = useCallback(() => { - const headers: Record = { 'Content-Type': 'application/json' }; - if (user?.id || user?._id) headers['x-user-id'] = user.id || user._id; - return headers; - }, [user]); - - // Data Fetching - const fetchData = useCallback(() => { - if (!objectName) return; - setLoading(true); - - const params = new URLSearchParams(); - if (searchTerm) { - let textFields: string[] = []; - - if (objectSchema?.fields) { - const fieldsArr = Array.isArray(objectSchema.fields) - ? objectSchema.fields - : Object.values(objectSchema.fields); - - textFields = fieldsArr - .filter((field: any) => !field.type || field.type === 'string') - .map((field: any) => field.name); - } else { - textFields = ['name', 'title', 'description', 'email']; - } - - if (textFields.length > 0) { - const searchFilters: any[] = []; - textFields.forEach((field, index) => { - // OR logic for simple search - if (index > 0) searchFilters.push('or'); - searchFilters.push([field, 'contains', searchTerm]); - }); - params.append('filters', JSON.stringify(searchFilters)); - } - } - - // Add default sort descending by created/updated if possible? - // params.append('sort', 'created:desc'); - - fetch(`/api/data/${objectName}?${params.toString()}`, { headers: getHeaders() }) - .then(async res => { - if (!res.ok) throw new Error(await res.text() || res.statusText); - return res.json(); - }) - .then(result => { - const items = Array.isArray(result) ? result : (result.list || result.data || result.value || []); - setData(items); - }) - .catch(err => { - console.error(err); - setData([]); - }) - .finally(() => setLoading(false)); - }, [objectName, searchTerm, objectSchema, getHeaders]); - - useEffect(() => { - fetchData(); - }, [objectName, fetchData]); // Re-fetch when objectName changes or fetchData logic changes - - const handleCreate = (formData: any) => { - fetch(`/api/data/${objectName}`, { - method: 'POST', - headers: getHeaders(), - body: JSON.stringify(formData) - }) - .then(async res => { - if (!res.ok) throw new Error(await res.text()); - return res.json(); - }) - .then(() => { - navigate('..'); - fetchData(); - }) - .catch(err => alert(err.message)); - } - - const onRowClick = (event: any) => { - const id = event.data?.id || event.data?._id; - if (id) navigate(`${id}`); - }; - - return ( -
-
-
-

{label}

- - {data.length} records - -
-
-
- - setSearchTerm(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && fetchData()} - className="pl-8" - /> -
- - -
-
- -
- {objectSchema ? ( - - ) : ( -
- Loading schema... -
- )} -
- - !open && navigate('..')} - > - - - New {label} - - {objectSchema && ( - navigate('..')} - /> - )} - - -
- ); -} diff --git a/apps/web/src/components/dashboard/ObjectNotFound.tsx b/apps/web/src/components/dashboard/ObjectNotFound.tsx deleted file mode 100644 index ce6d7274..00000000 --- a/apps/web/src/components/dashboard/ObjectNotFound.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useRouter } from '../../hooks/useRouter'; - -interface ObjectNotFoundProps { - objectName: string; -} - -export function ObjectNotFound({ objectName }: ObjectNotFoundProps) { - const { navigate } = useRouter(); - - return ( -
-
- -
-

Object Not Found

-

- The object "{objectName}" does not exist or you do not have permission to view it. -

- -
- ); -} diff --git a/apps/web/src/components/dashboard/SettingsView.tsx b/apps/web/src/components/dashboard/SettingsView.tsx deleted file mode 100644 index 3a34e1c3..00000000 --- a/apps/web/src/components/dashboard/SettingsView.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - Badge -} from '@objectos/ui'; - -interface SettingsViewProps { - objectCount?: number; -} - -export function SettingsView({ objectCount = 0 }: SettingsViewProps) { - return ( -
-
-
-

Settings

-

- Manage your server configuration and view system status. -

-
- - - About ObjectOS - System information and status. - - -
- Version - v0.2.0 -
-
- Environment - Development -
-
- Collections - {objectCount} -
-
-
-
-
- ); -} diff --git a/apps/web/src/components/dashboard/SidebarItem.tsx b/apps/web/src/components/dashboard/SidebarItem.tsx deleted file mode 100644 index e1b4db71..00000000 --- a/apps/web/src/components/dashboard/SidebarItem.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; - -interface SidebarItemProps { - icon: React.ComponentType<{ className?: string }>; - label: string; - active: boolean; - onClick: () => void; -} - -export function SidebarItem({ icon: Icon, label, active, onClick }: SidebarItemProps) { - return ( - - ); -} diff --git a/apps/web/src/components/nav-user.tsx b/apps/web/src/components/nav-user.tsx deleted file mode 100644 index 584545f7..00000000 --- a/apps/web/src/components/nav-user.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client" - -import { - Bell as BellIcon, - CreditCard as CreditCardIcon, - LogOut as LogOutIcon, - MoreVertical as MoreVerticalIcon, - User as UserCircleIcon, -} from "lucide-react" - -import { - Avatar, - AvatarFallback, - AvatarImage, - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@objectos/ui" -import { useAuth } from "../context/AuthContext" - -export function NavUser({ - user, -}: { - user: { - name: string - email: string - avatar?: string - } -}) { - const { isMobile } = useSidebar() - const { signOut } = useAuth(); - - return ( - - - - - - - - CN - -
- {user.name} - - {user.email} - -
- -
-
- - -
- - - CN - -
- {user.name} - - {user.email} - -
-
-
- - - { - window.history.pushState({}, '', '/settings'); - window.dispatchEvent(new Event('pushstate')); - }}> - - Settings - - { - window.history.pushState({}, '', '/organization'); - window.dispatchEvent(new Event('pushstate')); - }}> - - Organization - - - - Notifications - - - - - - Log out - -
-
-
-
- ) -} diff --git a/apps/web/src/components/ui/spinner.tsx b/apps/web/src/components/ui/spinner.tsx deleted file mode 100644 index 13309001..00000000 --- a/apps/web/src/components/ui/spinner.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { cn } from "@objectos/ui"; - -export function Spinner({ className }: { className?: string }) { - return ( -
- Loading... -
- ); -} diff --git a/apps/web/src/context/AuthContext.tsx b/apps/web/src/context/AuthContext.tsx deleted file mode 100644 index 50ec34ac..00000000 --- a/apps/web/src/context/AuthContext.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; -import { authClient } from '../lib/auth'; - -interface User { - id: string; - email: string; - name?: string; - [key: string]: any; -} - -interface Session { - session: { - id: string; - userId: string; - activeOrganizationId?: string | null; - [key: string]: any; - }; - user: User; -} - -interface AuthContextType { - user: User | null; - session: Session["session"] | null; - loading: boolean; - signIn: (email: string, password: string) => Promise; - signUp: (email: string, password: string, name: string) => Promise; - signOut: () => Promise; -} - -const AuthContext = createContext(undefined); - -export function AuthProvider({ children }: { children: React.ReactNode }) { - const [user, setUser] = useState(null); - const [session, setSession] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - authClient.getSession().then(({ data }) => { - if (data?.user) { - setUser(data.user); - setSession(data.session); - } else { - setUser(null); - setSession(null); - } - setLoading(false); - }).catch(() => setLoading(false)); - }, []); - - const signIn = async (email: string, password: string) => { - const { error } = await authClient.signIn.email({ - email, - password - }); - if (error) throw error; - // Refresh session to Ensure we get the full session object - const sessionData = await authClient.getSession(); - if (sessionData.data) { - setUser(sessionData.data.user); - setSession(sessionData.data.session); - } - }; - - const signUp = async (email: string, password: string, name: string) => { - const { error } = await authClient.signUp.email({ - email, - password, - name - }); - if (error) throw error; - // Refresh session - const sessionData = await authClient.getSession(); - if (sessionData.data) { - setUser(sessionData.data.user); - setSession(sessionData.data.session); - } - }; - - const signOut = async () => { - await authClient.signOut(); - setUser(null); - setSession(null); - }; - - return ( - - {children} - - ); -} - -export function useAuth() { - const context = useContext(AuthContext); - if (context === undefined) { - throw new Error('useAuth must be used within an AuthProvider'); - } - return context; -} diff --git a/apps/web/src/hooks/useObjectSchema.ts b/apps/web/src/hooks/useObjectSchema.ts deleted file mode 100644 index 91244eb3..00000000 --- a/apps/web/src/hooks/useObjectSchema.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { useState, useEffect } from 'react'; -import { getHeaders } from '../lib/api'; - -const schemaCache: Record = {}; - -export function useObjectSchema(objectName: string) { - const [schema, setSchema] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - if (!objectName) return; - - if (schemaCache[objectName]) { - setSchema(schemaCache[objectName]); - setLoading(false); - return; - } - - setLoading(true); - // We could fetch single object too: /api/metadata/object/:name - // But current API might be bulk. Let's try single if available or filter from bulk (inefficient but works for now) - // Optimization: Create /api/metadata/object/:name endpoint in backend or assume bulk cache in context. - // For now, let's fetch list and find. (Or just assume the API supports name, which standard ObjectQL usually does) - - fetch(`/api/metadata/object/${objectName}`, { headers: getHeaders() }) - .then(async res => { - if (res.status === 404) return null; // Not found - if (!res.ok) { - // Fallback to bulk if single endpoint fails? - // Let's try bulk list as fallback or primay if we know backend - throw new Error('Failed to load schema'); - } - return res.json(); - }) - .then(data => { - if (data) { - // Normalize fields from Array to Record if needed - if (data.fields && Array.isArray(data.fields)) { - const fieldRecord: Record = {}; - data.fields.forEach((f: any) => { - if (f.name) fieldRecord[f.name] = f; - }); - data.fields = fieldRecord; - } - - schemaCache[objectName] = data; - setSchema(data); - setLoading(false); - } else { - // Trigger fallback - throw new Error('Not found'); - } - }) - .catch(() => { - // Fallback: Fetch all - fetch('/api/metadata/object', { headers: getHeaders() }) - .then(res => res.json()) - .then(result => { - const list = Array.isArray(result) ? result : (result.object || result.data || []); - const found = list.find((o: any) => o.name === objectName); - if (found) { - schemaCache[objectName] = found; - setSchema(found); - setError(null); - } else { - setError(new Error('Object not found')); - } - }) - .catch(e => setError(e)) - .finally(() => setLoading(false)); - }); - - }, [objectName]); - - return { schema, loading, error }; -} diff --git a/apps/web/src/hooks/useRouter.ts b/apps/web/src/hooks/useRouter.ts deleted file mode 100644 index dd3bfd41..00000000 --- a/apps/web/src/hooks/useRouter.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useNavigate, useLocation } from 'react-router-dom'; - -export function useRouter() { - const navigate = useNavigate(); - const location = useLocation(); - - return { - path: location.pathname, - navigate: (path: string) => navigate(path), - search: location.search - }; -} diff --git a/apps/web/src/index.css b/apps/web/src/index.css deleted file mode 100644 index d4b50785..00000000 --- a/apps/web/src/index.css +++ /dev/null @@ -1 +0,0 @@ -@import 'tailwindcss'; diff --git a/apps/web/src/layouts/MainLayout.tsx b/apps/web/src/layouts/MainLayout.tsx deleted file mode 100644 index 4b46ee80..00000000 --- a/apps/web/src/layouts/MainLayout.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { - Avatar, - AvatarFallback, - AvatarImage, - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuGroup, - DropdownMenuItem -} from '@objectos/ui'; -import { LogOut, Settings as SettingsIcon, Building, Bell } from 'lucide-react'; -import { useAuth } from '../context/AuthContext'; -import { Outlet } from 'react-router-dom'; - -export function MainLayout() { - const { user, signOut } = useAuth(); - - return ( -
-
-
- ObjectOS -
-
- {/* User Menu */} - - - - - - -
- - - CN - -
- {user?.name} - {user?.email} -
-
-
- - - - - Organization - - - - Settings - - - - Notifications - - - - - - Log out - -
-
-
-
-
-
-

Apps

- -
-
-
- ); -} diff --git a/apps/web/src/layouts/WorkspaceLayout.tsx b/apps/web/src/layouts/WorkspaceLayout.tsx deleted file mode 100644 index f67ef149..00000000 --- a/apps/web/src/layouts/WorkspaceLayout.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useState, useEffect, useRef } from 'react'; -import { - SidebarProvider, - SidebarInset, - SidebarTrigger, - Separator, - Breadcrumb, - BreadcrumbItem, - BreadcrumbList, - BreadcrumbPage -} from '@objectos/ui'; -import { AppSidebar } from '../components/app-sidebar'; -import { Outlet, useLocation } from 'react-router-dom'; - -export function WorkspaceLayout() { - const location = useLocation(); - const [currentAppMetadata, setCurrentAppMetadata] = useState(null); - const lastFetchedApp = useRef(null); - - // Determines current app from URL parameters or path parsing - // Since this layout wraps routes like /app/:appName/*, we can try to extract it from location if useParams isn't populated yet by the parent? - // Actually, in clustered routes, useParams matches the current route match. - // If the Route is }>, then params.appName works. - // But if we use nested routes, we might need to parse. - - // Simplest: Check path parts - const appName = location.pathname.split('/')[2]; - const isAppRoute = location.pathname.startsWith('/app/'); - - useEffect(() => { - if (isAppRoute && appName) { - // Avoid re-fetching if we already have this app loaded - if (lastFetchedApp.current === appName) { - return; - } - - lastFetchedApp.current = appName; - - fetch(`/api/metadata/app/${appName}`) - .then(res => { - if (!res.ok) throw new Error('App not found'); - return res.json(); - }) - .then(data => { - setCurrentAppMetadata(data); - }) - .catch(err => { - console.error(err); - setCurrentAppMetadata(null); - lastFetchedApp.current = null; - }); - } else { - if (lastFetchedApp.current) { - setCurrentAppMetadata(null); - lastFetchedApp.current = null; - } - } - }, [isAppRoute, appName]); - - const getPageTitle = () => { - if (location.pathname === '/settings') return 'Settings'; - if (location.pathname === '/organization') return 'Organization'; - if (appName) return `App: ${appName}`; - return 'Dashboard'; - }; - - return ( - - - -
- - - - - - - {getPageTitle()} - - - - -
-
- -
-
-
- ); -} diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts deleted file mode 100644 index 0d7e44d5..00000000 --- a/apps/web/src/lib/api.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const getHeaders = () => { - const headers: Record = { 'Content-Type': 'application/json' }; - return headers; -}; diff --git a/apps/web/src/lib/auth.ts b/apps/web/src/lib/auth.ts deleted file mode 100644 index 904f147c..00000000 --- a/apps/web/src/lib/auth.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createAuthClient } from "better-auth/react" -import { organizationClient } from "better-auth/client/plugins" - -export const authClient = createAuthClient({ - baseURL: typeof window !== "undefined" ? window.location.origin + "/api/auth" : "http://localhost:3000/api/auth", - plugins: [ - organizationClient() - ] -}) diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts deleted file mode 100644 index d084ccad..00000000 --- a/apps/web/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx deleted file mode 100644 index 21e96b9c..00000000 --- a/apps/web/src/main.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import { BrowserRouter } from 'react-router-dom' -import App from './App' -import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'; - -// Register AG Grid Modules -ModuleRegistry.registerModules([ AllCommunityModule ]); - -import 'ag-grid-community/styles/ag-grid.css' -import 'ag-grid-community/styles/ag-theme-alpine.css' -import '@objectos/ui/dist/index.css' -import './index.css' - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - , -) diff --git a/apps/web/src/pages/AppDashboard.tsx b/apps/web/src/pages/AppDashboard.tsx deleted file mode 100644 index 4560c3cf..00000000 --- a/apps/web/src/pages/AppDashboard.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useState, useEffect } from 'react'; -import { useParams } from 'react-router-dom'; -import { - Card, - Spinner -} from '@objectos/ui'; -import { useRouter } from '../hooks/useRouter'; -import { DynamicIcon } from '../components/DynamicIcon'; -import { getHeaders } from '../lib/api'; - -export default function AppDashboard() { - const { appName } = useParams(); - const { navigate } = useRouter(); - const [app, setApp] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - if (!appName) return; - setLoading(true); - fetch(`/api/metadata/app/${appName}`, { headers: getHeaders() }) - .then(res => { - if (!res.ok) throw new Error('App not found'); - return res.json(); - }) - .then(data => { - setApp(data); - setLoading(false); - }) - .catch(err => { - console.error(err); - setLoading(false); - }); - }, [appName]); - - if (loading) return
; - if (!app) return
App not found
; - - // Helper to resolve absolute vs relative paths - const resolveLink = (item: any) => { - if (item.object) return `/app/${appName}/object/${item.object}`; - if (item.page) return `/app/${appName}/page/${item.page}`; - if (item.url) return item.url; - return '#'; - }; - - const MenuItemCard = ({ item }: { item: any }) => ( - navigate(resolveLink(item))} - > -
- -
- {item.label || item.object || 'Item'} - {item.description && {item.description}} -
- ); - - const MenuSection = ({ section }: { section: any }) => { - const title = section.label; - const items = section.items || []; - - if (!items.length) return null; - - return ( -
- {title && !section._isDefaultWrapper && ( -

- {section.icon && } - {title} -

- )} -
- {items.map((item: any, idx: number) => ( - - ))} -
-
- ); - }; - - // Prepare sections - const rawMenu = app.menu; - const isSection = (item: any) => item && item.items && Array.isArray(item.items) && !item.type && !item.object && !item.url; - const isGrouped = Array.isArray(rawMenu) && rawMenu.length > 0 && isSection(rawMenu[0]); - const sections = rawMenu ? (isGrouped ? rawMenu : [{ label: 'Menu', items: rawMenu, _isDefaultWrapper: true }]) : []; - - return ( -
-
-
-
- -
-
-

{app.label || app.name}

-

{app.description || `Welcome to ${app.label || app.name}`}

-
-
-
- - {sections.length > 0 ? ( - sections.map((section: any, idx: number) => ( - - )) - ) : ( -
-

This app has no menu items configured.

-
- )} -
- ); -} diff --git a/apps/web/src/pages/AppList.tsx b/apps/web/src/pages/AppList.tsx deleted file mode 100644 index 07c5c77e..00000000 --- a/apps/web/src/pages/AppList.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useState, useEffect } from 'react'; -import { - Card, - CardHeader, - CardTitle, - CardDescription, - CardContent, - Button, - Spinner -} from '@objectos/ui'; -import { useRouter } from '../hooks/useRouter'; -import { getHeaders } from '../lib/api'; -import { AppWindow, ChevronRight } from 'lucide-react'; -import { DynamicIcon } from '../components/DynamicIcon'; - -interface App { - id?: string; - name: string; - label?: string; // App Label for display - description?: string; - code?: string; - icon?: string; - color?: string; - dark?: boolean; - menu?: any[]; -} - -export default function AppList() { - const [apps, setApps] = useState([]); - const [loading, setLoading] = useState(true); - const { navigate } = useRouter(); - - useEffect(() => { - fetch('/api/metadata/app', { headers: getHeaders() }) - .then(res => res.json()) - .then(data => { - // Handle wrapped response { app: [...] } or direct array [...] - const appList = Array.isArray(data) ? data : (data.app || []); - if (Array.isArray(appList)) { - setApps(appList); - } - setLoading(false); - }) - .catch(err => { - console.error('Failed to load apps', err); - setLoading(false); - }); - }, []); - - if (loading) { - return
; - } - - return ( -
-

My Apps

-

Select an application to start working

- - {apps.length === 0 ? ( -
- -

No apps found

-

- Get started by creating your first application in the backend configuration. -

-
- ) : ( -
- {apps.map((app, idx) => { - const appCode = app.code || app.id || app.name; // Fallback for code - const appName = app.label || app.name; // Use label if available, fallback to name - const appColor = app.color; - const key = app.id || app.code || `app-${idx}`; - - return ( - navigate(`/app/${appCode}`)} - > - {/* Color strip on top or side if defined */} - {appColor && ( -
- )} - - -
-
- -
- -
- {appName} - - {app.description || 'No description provided.'} - -
- - {/* Additional info bits */} - - - ); - })} -
- )} -
- ); -} diff --git a/apps/web/src/pages/Dashboard.tsx b/apps/web/src/pages/Dashboard.tsx deleted file mode 100644 index 29a6fed3..00000000 --- a/apps/web/src/pages/Dashboard.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Spinner } from '@objectos/ui'; -import { useAuth } from '../context/AuthContext'; -import { useRouter } from '../hooks/useRouter'; -import { ObjectListView } from '../components/dashboard/ObjectListView'; -import { ObjectDetailView } from '../components/dashboard/ObjectDetailView'; -import { SettingsView } from '../components/dashboard/SettingsView'; -import { ObjectNotFound } from '../components/dashboard/ObjectNotFound'; -import { DashboardHome } from './DashboardHome'; -import { getHeaders } from '../lib/api'; - -export default function Dashboard() { - const { user } = useAuth(); - const [loading, setLoading] = useState(true); - const [objects, setObjects] = useState>({}); - const [apps, setApps] = useState([]); - const { path, navigate } = useRouter(); - - // Parse path - // Support patterns: - // 1. /object/:objectName/:recordId? - // 2. /app/:appName/object/:objectName/:recordId? - const parts = path.split('/'); - - let objectName: string | null = null; - let recordId: string | undefined = undefined; - let appName: string | null = null; - - if (parts[1] === 'object') { - objectName = parts[2]; - recordId = parts[3]; - } else if (parts[1] === 'app' && parts[3] === 'object') { - appName = parts[2]; - objectName = parts[4]; - recordId = parts[5]; - } - - // Wrap navigate to preserve app context - const wrappedNavigate = (to: string) => { - if (appName && to.startsWith('/object/')) { - navigate(to.replace('/object/', `/app/${appName}/object/`)); - } else { - navigate(to); - } - }; - - useEffect(() => { - if (user) { - fetch('/api/data/app?limit=100', { headers: getHeaders() }) - .then(res => res.json()) - .then(result => { - const data = Array.isArray(result) ? result : (result.data || []); - setApps(data); - }) - .catch(console.error); - - // Fetch objects - fetch('/api/metadata/object', { headers: getHeaders() }) - .then(res => res.json()) - .then(result => { - // Handle potential wrapper format { object: [...] } or { data: [...] } - const list = Array.isArray(result) ? result : (result.object || result.data || []); - - // Convert array to map - const objectsMap: Record = {}; - if (Array.isArray(list)) { - list.forEach((obj: any) => { - if (obj && obj.name) { - objectsMap[obj.name] = obj; - } - }); - } - - setObjects(objectsMap); - setLoading(false); - }) - .catch(err => { - console.error("Failed to fetch objects", err); - setLoading(false); - }); - } - }, [user]); - - if (loading) { - return ( -
- -
- ); - } - - // Since Dashboard is now rendered INSIDE App.tsx's Layout, - // we only need to render the content specific to the route - - if (path === '/settings') { - return ; - } - - // Handle Object Routes - if (objectName) { - if (objects[objectName]) { - if (recordId) { - return ( - - ); - } - return ( - - ); - } else { - // Object requested but not found in metadata - return ; - } - } - - // Default Dashboard View (App Selection) - return ; -} diff --git a/apps/web/src/pages/DashboardHome.tsx b/apps/web/src/pages/DashboardHome.tsx deleted file mode 100644 index f6a76e09..00000000 --- a/apps/web/src/pages/DashboardHome.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { - Card, - CardHeader, - CardTitle, - CardDescription -} from '@objectos/ui'; -import { useRouter } from '../hooks/useRouter'; - -interface App { - id?: string; - _id?: string; - name: string; - slug?: string; - description?: string; - icon?: string; - objects?: string[]; -} - -interface DashboardHomeProps { - apps: App[]; -} - -export function DashboardHome({ apps }: DashboardHomeProps) { - const { navigate } = useRouter(); - - return ( -
-
-
-

Applications

-

- Select an application to start working. -

-
-
- -
- {apps.map(app => ( - { - // Navigate to first object or app root - if (app.objects && Array.isArray(app.objects) && app.objects.length > 0) { - navigate(`/app/${app.slug || app.name}/object/${app.objects[0]}`); - } else { - navigate(`/app/${app.slug || app.name}`); - } - }} - > - -
- -
-
- {app.name} - - {app.description || 'No description provided'} - -
-
-
- ))} - - {apps.length === 0 && ( -
-
- -
-

No Applications Found

-

Admin needs to create an application first.

-
- )} -
-
- ); -} diff --git a/apps/web/src/pages/Login.tsx b/apps/web/src/pages/Login.tsx deleted file mode 100644 index 1994d548..00000000 --- a/apps/web/src/pages/Login.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useState } from 'react'; -import { Card, Input, Button, Label, Spinner } from '@objectos/ui'; -import { useAuth } from '../context/AuthContext'; -import { Database } from 'lucide-react'; - -export default function Login() { - const { signIn, signUp } = useAuth(); - const [isSignIn, setIsSignIn] = useState(true); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [name, setName] = useState(''); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - setLoading(true); - try { - if (isSignIn) { - await signIn(email, password); - } else { - await signUp(email, password, name); - } - // App component handles redirection via auth state change - } catch (err: any) { - console.error(err); - setError(err.message || err.error?.message || 'Authentication failed'); - setLoading(false); - } - }; - - return ( -
- {/* Decorative background elements */} -
- -
-
-
-
- -
-
-

- {isSignIn ? 'Welcome back' : 'Create account'} -

-

- {isSignIn ? 'Enter your details to access your workspace' : 'Start your journey with ObjectOS'} -

-
- - -
- {!isSignIn && ( -
- - setName(e.target.value)} - placeholder="Jane Doe" - required - /> -
- )} -
- - setEmail(e.target.value)} - placeholder="name@example.com" - required - /> -
-
- - setPassword(e.target.value)} - placeholder="••••••••" - required - /> -
- - {error && ( -
- {error} -
- )} - - -
-
- -
- {isSignIn ? "New to ObjectQL? " : "Already have an account? "} - -
-
-
- ); -} diff --git a/apps/web/src/pages/Organization.tsx b/apps/web/src/pages/Organization.tsx deleted file mode 100644 index 8e2f4df0..00000000 --- a/apps/web/src/pages/Organization.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Card, Input, Label, Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@objectos/ui'; -import { useAuth } from '../context/AuthContext'; -import { authClient } from '../lib/auth'; - -export default function Organization() { - const { session } = useAuth(); - const [organizations, setOrganizations] = useState([]); - const [activeOrg, setActiveOrg] = useState(null); - const [members, setMembers] = useState([]); - const [loading, setLoading] = useState(true); - - // Create Org Form - const [newOrgName, setNewOrgName] = useState(''); - const [newOrgSlug, setNewOrgSlug] = useState(''); - - // Invite Member Form - const [inviteEmail, setInviteEmail] = useState(''); - const [inviteRole, setInviteRole] = useState('user'); - - useEffect(() => { - loadOrganizations(); - }, [session?.activeOrganizationId]); - - const loadOrganizations = async () => { - try { - const { data: orgs } = await authClient.organization.list(); - setOrganizations(orgs || []); - - if (session?.activeOrganizationId) { - const current = orgs?.find(o => o.id === session.activeOrganizationId); - setActiveOrg(current); - if (current) loadMembers(); - } - } catch (e) { - console.error(e); - } finally { - setLoading(false); - } - }; - - const loadMembers = async () => { - // listMembers uses the current active organization from session/headers - const { data } = await authClient.organization.listMembers(); - setMembers(data?.members || []); - }; - - const createOrg = async () => { - const { data, error } = await authClient.organization.create({ - name: newOrgName, - slug: newOrgSlug - }); - if (error) alert(error.message); - if (data) { - await authClient.organization.setActive({ organizationId: data.id }); - window.location.reload(); // Simple reload to refresh context - } - }; - - const inviteMember = async () => { - if (!activeOrg) return; - const { data, error } = await authClient.organization.inviteMember({ - email: inviteEmail, - role: inviteRole as "member" | "admin" | "owner", - }); - if (error) alert(error.message); - if (data) { - setInviteEmail(''); - alert('Invitation sent!'); - } - }; - - if (loading) return
Loading...
; - - return ( -
-
-

Organization Management

- {!activeOrg && ( - - - - - - - Create New Organization - -
-
- - setNewOrgName(e.target.value)} /> -
-
- - setNewOrgSlug(e.target.value)} /> -
- -
-
-
- )} -
- - {!activeOrg ? ( - -

You are not currently in an active organization.

- {organizations.length > 0 && ( -
- -
- {organizations.map(org => ( - - ))} -
-
- )} -
- ) : ( - <> - -
-
-

{activeOrg.name}

-

Slug: {activeOrg.slug}

-
- -
-
- - -
-

Members

- - - - - - - Invite Member - -
-
- - setInviteEmail(e.target.value)} /> -
-
- - setInviteRole(e.target.value)} /> -
- -
-
-
-
- - - - - User - Email - Role - Joined - - - - {members.map(member => ( - - {member.user.name} - {member.user.email} - {member.role} - {new Date(member.createdAt).toLocaleDateString()} - - ))} - -
-
- - )} -
- ); -} diff --git a/apps/web/src/pages/Settings.tsx b/apps/web/src/pages/Settings.tsx deleted file mode 100644 index d56f29c8..00000000 --- a/apps/web/src/pages/Settings.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Card, Input, Label, Button } from '@objectos/ui'; -import { useAuth } from '../context/AuthContext'; - -export default function Settings() { - const { user, signOut } = useAuth(); - - return ( -
-

Account Settings

- - -

Profile Information

-
-
- - -
-
- - -
-
- - -
-
-
- - -

Danger Zone

-

- Sign out of your account on this device. -

- -
-
- ); -} diff --git a/apps/web/src/pages/objects/ObjectDetailRoute.tsx b/apps/web/src/pages/objects/ObjectDetailRoute.tsx deleted file mode 100644 index 5eb59e31..00000000 --- a/apps/web/src/pages/objects/ObjectDetailRoute.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useParams } from 'react-router-dom'; -import { ObjectDetailView } from '../../components/dashboard/ObjectDetailView'; -import { useObjectSchema } from '../../hooks/useObjectSchema'; -import { ObjectNotFound } from '../../components/dashboard/ObjectNotFound'; -import { Spinner } from '@objectos/ui'; -import { useRouter } from '../../hooks/useRouter'; - -export function ObjectDetailRoute() { - const { objectName, recordId } = useParams(); // Matches /view/:recordId - // Also support legacy param if needed by checking path, but let's stick to standard params - const id = recordId; - - const { schema, loading, error } = useObjectSchema(objectName || ''); - const { navigate } = useRouter(); - - if (loading) return
; - if (error || !schema) return ; - - return ( - - ); -} diff --git a/apps/web/src/pages/objects/ObjectListRoute.tsx b/apps/web/src/pages/objects/ObjectListRoute.tsx deleted file mode 100644 index 6d057a6a..00000000 --- a/apps/web/src/pages/objects/ObjectListRoute.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useParams } from 'react-router-dom'; -import { ObjectListView } from '../../components/dashboard/ObjectListView'; -import { useObjectSchema } from '../../hooks/useObjectSchema'; -import { useAuth } from '../../context/AuthContext'; -import { ObjectNotFound } from '../../components/dashboard/ObjectNotFound'; -import { Spinner } from '@objectos/ui'; -import { useRouter } from '../../hooks/useRouter'; - -export function ObjectListRoute({ isCreating = false }: { isCreating?: boolean }) { - const { objectName } = useParams(); - const { schema, loading, error } = useObjectSchema(objectName || ''); - const { user } = useAuth(); - const { navigate } = useRouter(); - - if (loading) return
; - - // In React Router v6, error boundary is better, but simple check works - if (error || !schema) return ; - - return ( - - ); -} diff --git a/apps/web/src/routes.ts b/apps/web/src/routes.ts deleted file mode 100644 index 61e254c2..00000000 --- a/apps/web/src/routes.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Root Routes -export const ROOT = '/'; -export const LOGIN = '/login'; -export const APPS = '/apps'; - -// App Routes (Workspace) -export const APP_ROOT = '/app/:appName'; -export const APP_DASHBOARD = '/app/:appName/dashboard'; -export const APP_OBJECT_LIST = '/app/:appName/object/:objectName'; -export const APP_OBJECT_DETAIL = '/app/:appName/object/:objectName/view/:recordId'; -export const APP_OBJECT_EDIT = '/app/:appName/object/:objectName/edit/:recordId'; -export const APP_OBJECT_NEW = '/app/:appName/object/:objectName/new'; -export const APP_PAGE = '/app/:appName/page/:pageId'; - -// Global Routes (Legacy/Global Context) -export const OBJECT_LIST = '/object/:objectName'; -export const OBJECT_DETAIL = '/object/:objectName/view/:recordId'; // Standardized view/edit -export const SETTINGS = '/settings'; -export const ORGANIZATION = '/organization'; -export const USER_PROFILE = '/user/profile'; - -// Route Generators -export const routes = { - root: () => ROOT, - login: () => LOGIN, - apps: () => APPS, - app: (appName: string) => `/app/${appName}`, - objectList: (appName: string, objectName: string) => `/app/${appName}/object/${objectName}`, - objectDetail: (appName: string, objectName: string, id: string) => `/app/${appName}/object/${objectName}/view/${id}`, - objectEdit: (appName: string, objectName: string, id: string) => `/app/${appName}/object/${objectName}/edit/${id}`, - objectNew: (appName: string, objectName: string) => `/app/${appName}/object/${objectName}/new`, -}; diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js deleted file mode 100644 index bd8131e2..00000000 --- a/apps/web/tailwind.config.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], -} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json deleted file mode 100644 index 3934b8f6..00000000 --- a/apps/web/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/apps/web/tsconfig.node.json b/apps/web/tsconfig.node.json deleted file mode 100644 index 9b748374..00000000 --- a/apps/web/tsconfig.node.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "strict": true, - "noEmit": false, - "emitDeclarationOnly": true - }, - "include": ["vite.config.ts"] -} diff --git a/apps/web/vite.config.d.ts b/apps/web/vite.config.d.ts deleted file mode 100644 index 340562af..00000000 --- a/apps/web/vite.config.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const _default: import("vite").UserConfig; -export default _default; diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts deleted file mode 100644 index a883de68..00000000 --- a/apps/web/vite.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import path from 'path' - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react() as any], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, - server: { - proxy: { - '/api': { - target: 'http://localhost:3000', - changeOrigin: true, - } - } - } -}) diff --git a/docs/SPEC_REFACTORING.md b/docs/SPEC_REFACTORING.md index 716b04b4..83c4050e 100644 --- a/docs/SPEC_REFACTORING.md +++ b/docs/SPEC_REFACTORING.md @@ -2,7 +2,7 @@ ## Overview -This document describes the refactoring of ObjectOS to align with the [@objectstack/spec](https://www.npmjs.com/package/@objectstack/spec) protocol version 0.3.3. +This document describes the refactoring of ObjectOS to align with the [@objectstack/spec](https://github.com/objectstack-ai/spec) protocol version 0.3.3. ## What is @objectstack/spec? @@ -253,7 +253,7 @@ See `packages/kernel/src/plugins/example-spec-plugin.ts` for a complete, product ## Resources -- [@objectstack/spec on npm](https://www.npmjs.com/package/@objectstack/spec) +- [@objectstack/spec on GitHub](https://github.com/objectstack-ai/spec) - [Example Plugin](../packages/kernel/src/plugins/example-spec-plugin.ts) - [Kernel README](../packages/kernel/README.md) - [Architecture Documentation](../ARCHITECTURE.md) diff --git a/docs/guide/index.md b/docs/guide/index.md index 88a4fa8d..22bba5e7 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -21,7 +21,6 @@ ObjectOS is a **metadata-driven runtime engine** that interprets and executes bu │ ObjectOS (Runtime Repository - This One) │ │ - @objectos/kernel: Execution engine │ │ - @objectos/server: HTTP API layer │ -│ - @objectos/ui: React components │ └─────────────────────┬───────────────────────────┘ │ ▼ @@ -302,22 +301,9 @@ curl -X POST http://localhost:3000/api/data/contacts \ }' ``` -## Using the UI Components +## Using UI Components -ObjectOS provides React components that automatically render based on metadata: - -```tsx -import { ObjectGrid, ObjectForm } from '@objectos/ui'; - -function ContactsPage() { - return ( -
-

Contacts

- {/* Automatically generates a data grid */} - -
- ); -} +**Note**: The UI components have been moved to a separate project. They can be integrated with ObjectOS through the HTTP API layer (`@objectos/server`) which provides REST endpoints for metadata and data access. function ContactDetail({ contactId }) { return ( diff --git a/docs/guide/logic-actions.md b/docs/guide/logic-actions.md index f61a56d3..c12455c9 100644 --- a/docs/guide/logic-actions.md +++ b/docs/guide/logic-actions.md @@ -464,20 +464,24 @@ const result = await kernel.executeAction('contacts.sendEmail', { ### From UI -```typescript -import { useAction } from '@objectos/ui'; +> **Note**: The UI components have been moved to a separate project. The example below shows the conceptual pattern for invoking actions from a UI application. +```typescript +// Example pattern for UI integration function ContactDetail({ contactId }) { - const sendEmail = useAction('contacts.sendEmail'); - const handleSendEmail = async () => { - const result = await sendEmail({ - id: contactId, - subject: 'Follow up', - body: 'Thank you for your interest' + const result = await fetch('/api/actions/contacts.sendEmail', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: contactId, + subject: 'Follow up', + body: 'Thank you for your interest' + }) }); - alert(result.message); + const data = await result.json(); + alert(data.message); }; return ( diff --git a/docs/guide/platform-components.md b/docs/guide/platform-components.md index f9a16620..7e1fa814 100644 --- a/docs/guide/platform-components.md +++ b/docs/guide/platform-components.md @@ -40,7 +40,7 @@ The `@objectos/server` package is the Gateway. It translates HTTP/WebSockets int | :--- | :--- | :--- | | **ObjectQLController** | Generic REST endpoint for all objects. | `GET /:objectName/*`. No need to write manual controllers for new objects. | | **AuthProvider** | Authentication strategy manager. | Wraps `better-auth`. Supports pluggable strategies (GitHub, Google, SSO). | -| **StaticServeModule** | Hosting the compiled frontend. | Resolves `@objectos/web` dist path dynamically for production deployments. | +| **StaticServeModule** | Hosting the compiled frontend. | Serves static assets for frontend applications. | | **ExceptionFilter** | Standardized error formatting. | Converts `ObjectOSError` into JSON: `{ error: { code: 404, message: "..." } }`. | ### Functional Realization: "Context-Aware Request" @@ -51,18 +51,9 @@ The `@objectos/server` package is the Gateway. It translates HTTP/WebSockets int ## 🖥️ 3. Interaction Support (UI Layer) -The `@objectos/ui` (Components) and `@objectos/web` (App) packages provide the human interface. +> **Note**: The UI components have been moved to a separate project and are no longer part of this monorepo. -### Component Breakdown - -| Component | Responsibility | Implementation Notes | -| :--- | :--- | :--- | -| **ObjectGrid** | Data Table with "Excel-like" features. | Uses **TanStack Table**. Implements Virtual Scroll for 100k+ rows. | -| **ObjectForm** | Dynamic Record Editor. | Uses **React-Hook-Form**. Generates Zod schema from Metadata at runtime. | -| **LayoutShell** | Application chrome (Sidebar, Header). | Responsive. Adapts menu based on user permissions. | -| **DataQueryHook** | React Query wrapper for API. | Cache management. `useQuery(['data', 'contacts'], ...)` | - -### Functional Realization: "Dynamic Types" +The UI layer provides the human interface, integrating with ObjectOS through the HTTP API exposed by `@objectos/server`. * **Design**: The UI downloads metadata initially. * **Flow**: `schema.json` received -> `FieldFactory` maps `type: 'date'` to `` -> Renders Cell. diff --git a/docs/guide/ui-framework.md b/docs/guide/ui-framework.md index 5d14d994..308d60d0 100644 --- a/docs/guide/ui-framework.md +++ b/docs/guide/ui-framework.md @@ -1,6 +1,8 @@ # Standard UI Components Reference -This document defines the standard component library for `@objectos/ui`. These components are the reference implementations for the **View & Layout Specifications**. +> **Note**: The UI components (`@objectos/ui`) have been moved to a separate project and are no longer part of this monorepo. This document is kept for reference and describes the design principles for UI components that integrate with ObjectOS. + +This document defines the standard component library for UI components. These components are the reference implementations for the **View & Layout Specifications**. --- diff --git a/package.json b/package.json index 1e70b9aa..480c7f89 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,8 @@ ] }, "scripts": { - "dev": "concurrently \"pnpm run server\" \"pnpm run web:watch\" --kill-others --names \"SERVER,WEB\" -c \"magenta,blue\"", + "dev": "pnpm --filter @objectos/server dev", "server": "pnpm --filter @objectos/server dev", - "web": "pnpm --filter @objectos/web dev", - "web:watch": "pnpm --filter @objectos/web build:watch", "build": "tsc -b && pnpm -r build", "start": "pnpm --filter @objectos/server start:prod", "test": "pnpm -r test", diff --git a/packages/kernel/README.md b/packages/kernel/README.md index fb2f7f1d..485385f3 100644 --- a/packages/kernel/README.md +++ b/packages/kernel/README.md @@ -1,6 +1,6 @@ # @objectos/kernel -The core runtime engine for ObjectOS - a metadata-driven platform built on the [@objectstack/spec](https://www.npmjs.com/package/@objectstack/spec) protocol. +The core runtime engine for ObjectOS - a metadata-driven platform built on the [@objectstack/spec](https://github.com/objectstack-ai/spec) protocol. ## Overview diff --git a/packages/server/package.json b/packages/server/package.json index 9067d4dd..72605f4b 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -21,7 +21,6 @@ "@nestjs/serve-static": "^4.0.2", "@objectos/kernel": "workspace:*", "@objectos/preset-base": "workspace:*", - "@objectos/web": "workspace:*", "@objectql/core": "^3.0.1", "@objectql/driver-sql": "^3.0.1", "@objectql/driver-mongo": "^3.0.1", diff --git a/packages/server/src/app.module.ts b/packages/server/src/app.module.ts index 348ea2c1..770147e2 100644 --- a/packages/server/src/app.module.ts +++ b/packages/server/src/app.module.ts @@ -3,22 +3,14 @@ import { AppController } from './app.controller.js'; import { AppService } from './app.service.js'; import { ObjectQLModule } from './objectql/objectql.module.js'; import { AuthModule } from './auth/auth.module.js'; -import { ServeStaticModule } from '@nestjs/serve-static'; -import { join, resolve, dirname } from 'path'; import { AuthMiddleware } from './auth/auth.middleware.js'; import { ObjectOS } from '@objectos/kernel'; import { createRESTHandler, createMetadataHandler, createNodeHandler } from '@objectql/server'; -const clientDistPath = resolve(dirname(require.resolve('@objectos/web/package.json')), 'dist'); - @Module({ imports: [ ObjectQLModule, AuthModule, - ServeStaticModule.forRoot({ - rootPath: clientDistPath, - exclude: ['/api/(.*)'], - }), ], controllers: [AppController], providers: [AppService], diff --git a/packages/ui/components.json b/packages/ui/components.json deleted file mode 100644 index dbd2a8fc..00000000 --- a/packages/ui/components.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.js", - "css": "src/styles.css", - "baseColor": "slate", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - } -} diff --git a/packages/ui/examples/BasicDynamicForm.tsx b/packages/ui/examples/BasicDynamicForm.tsx deleted file mode 100644 index b30a0497..00000000 --- a/packages/ui/examples/BasicDynamicForm.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Basic DynamicForm Example - * - * This example demonstrates the simplest usage of DynamicForm - * without sections or tabs. - */ - -import React from 'react'; -import { DynamicForm } from '@objectos/ui'; -import type { ObjectConfig } from '@objectql/types'; - -const userConfig: ObjectConfig = { - name: 'user', - label: 'User', - fields: { - firstName: { - name: 'firstName', - label: 'First Name', - type: 'text', - required: true, - max_length: 50, - }, - lastName: { - name: 'lastName', - label: 'Last Name', - type: 'text', - required: true, - max_length: 50, - }, - email: { - name: 'email', - label: 'Email Address', - type: 'email', - required: true, - }, - phone: { - name: 'phone', - label: 'Phone Number', - type: 'phone', - }, - age: { - name: 'age', - label: 'Age', - type: 'number', - min: 18, - max: 100, - }, - bio: { - name: 'bio', - label: 'Biography', - type: 'textarea', - max_length: 500, - help_text: 'Tell us about yourself', - }, - newsletter: { - name: 'newsletter', - label: 'Subscribe to newsletter', - type: 'boolean', - defaultValue: true, - }, - }, -}; - -export function BasicDynamicFormExample() { - const handleSubmit = async (data: any) => { - console.log('Form submitted:', data); - - // Simulate API call - await new Promise(resolve => setTimeout(resolve, 1000)); - - alert('User created successfully!'); - }; - - const handleCancel = () => { - console.log('Form cancelled'); - }; - - return ( -
-

Create New User

- - -
- ); -} diff --git a/packages/ui/examples/ConditionalForm.tsx b/packages/ui/examples/ConditionalForm.tsx deleted file mode 100644 index 20b7a6fa..00000000 --- a/packages/ui/examples/ConditionalForm.tsx +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Conditional Form Example - * - * This example demonstrates field dependencies and conditional visibility. - * Fields appear/disappear based on other field values. - */ - -import React from 'react'; -import { DynamicForm } from '@objectos/ui'; -import type { ObjectConfig } from '@objectql/types'; -import type { FieldDependency } from '@objectos/ui'; - -const applicationConfig: ObjectConfig = { - name: 'application', - label: 'Job Application', - fields: { - // Basic Info - fullName: { - name: 'fullName', - label: 'Full Name', - type: 'text', - required: true, - }, - email: { - name: 'email', - label: 'Email', - type: 'email', - required: true, - }, - - // Employment Status - currentlyEmployed: { - name: 'currentlyEmployed', - label: 'Are you currently employed?', - type: 'boolean', - }, - currentEmployer: { - name: 'currentEmployer', - label: 'Current Employer', - type: 'text', - }, - currentJobTitle: { - name: 'currentJobTitle', - label: 'Current Job Title', - type: 'text', - }, - noticePeriod: { - name: 'noticePeriod', - label: 'Notice Period (days)', - type: 'number', - min: 0, - }, - - // Education - highestDegree: { - name: 'highestDegree', - label: 'Highest Degree', - type: 'select', - options: [ - { label: 'High School', value: 'highschool' }, - { label: 'Bachelor\'s', value: 'bachelors' }, - { label: 'Master\'s', value: 'masters' }, - { label: 'PhD', value: 'phd' }, - ], - required: true, - }, - university: { - name: 'university', - label: 'University/College Name', - type: 'text', - }, - graduationYear: { - name: 'graduationYear', - label: 'Graduation Year', - type: 'number', - min: 1950, - max: 2030, - }, - - // Relocation - willingToRelocate: { - name: 'willingToRelocate', - label: 'Willing to relocate?', - type: 'boolean', - }, - preferredLocations: { - name: 'preferredLocations', - label: 'Preferred Locations', - type: 'textarea', - help_text: 'List cities you would consider', - }, - - // Sponsorship - requiresSponsorship: { - name: 'requiresSponsorship', - label: 'Do you require visa sponsorship?', - type: 'boolean', - }, - visaType: { - name: 'visaType', - label: 'Current Visa Type', - type: 'text', - }, - }, -}; - -const fieldDependencies: Record = { - // Only show current employment fields if currently employed - currentEmployer: { - dependsOn: 'currentlyEmployed', - condition: (value) => value === true, - }, - currentJobTitle: { - dependsOn: 'currentlyEmployed', - condition: (value) => value === true, - }, - noticePeriod: { - dependsOn: 'currentlyEmployed', - condition: (value) => value === true, - }, - - // Only show university/year for degree holders - university: { - dependsOn: 'highestDegree', - condition: (value) => value && value !== 'highschool', - }, - graduationYear: { - dependsOn: 'highestDegree', - condition: (value) => value && value !== 'highschool', - }, - - // Only show location preferences if willing to relocate - preferredLocations: { - dependsOn: 'willingToRelocate', - condition: (value) => value === true, - }, - - // Only show visa type if requires sponsorship - visaType: { - dependsOn: 'requiresSponsorship', - condition: (value) => value === true, - }, -}; - -export function ConditionalFormExample() { - const handleSubmit = async (data: any) => { - console.log('Application submitted:', data); - await new Promise(resolve => setTimeout(resolve, 1000)); - alert('Application submitted successfully!'); - }; - - return ( -
-

Job Application Form

-

- Notice how fields appear and disappear based on your answers! -

- - -
- ); -} diff --git a/packages/ui/examples/ObjectFormExample.tsx b/packages/ui/examples/ObjectFormExample.tsx deleted file mode 100644 index 1330f796..00000000 --- a/packages/ui/examples/ObjectFormExample.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import React, { useState } from 'react'; -import { ObjectForm } from '@objectos/ui'; -import type { ObjectConfig } from '@objectql/types'; - -/** - * Example usage of ObjectForm component - * This demonstrates how to use the metadata-driven form - */ - -// Define object metadata -const taskObjectConfig: ObjectConfig = { - name: 'task', - label: 'Task', - description: 'Task management object', - fields: { - title: { - name: 'title', - label: 'Title', - type: 'text', - required: true, - max_length: 200, - }, - description: { - name: 'description', - label: 'Description', - type: 'textarea', - help_text: 'Provide a detailed description of the task', - }, - status: { - name: 'status', - label: 'Status', - type: 'select', - options: [ - { label: 'To Do', value: 'todo' }, - { label: 'In Progress', value: 'in_progress' }, - { label: 'Done', value: 'done' }, - ], - defaultValue: 'todo', - required: true, - }, - priority: { - name: 'priority', - label: 'Priority', - type: 'select', - options: [ - { label: 'Low', value: 'low' }, - { label: 'Medium', value: 'medium' }, - { label: 'High', value: 'high' }, - ], - defaultValue: 'medium', - }, - assignee: { - name: 'assignee', - label: 'Assignee', - type: 'lookup', - reference_to: 'user', - help_text: 'Select a user to assign this task to', - }, - is_completed: { - name: 'is_completed', - label: 'Mark as Completed', - type: 'boolean', - defaultValue: false, - }, - due_date: { - name: 'due_date', - label: 'Due Date', - type: 'date', - }, - progress: { - name: 'progress', - label: 'Progress (%)', - type: 'percent', - min: 0, - max: 100, - defaultValue: 0, - }, - estimated_hours: { - name: 'estimated_hours', - label: 'Estimated Hours', - type: 'number', - min: 0, - help_text: 'Estimated time to complete this task', - }, - budget: { - name: 'budget', - label: 'Budget', - type: 'currency', - min: 0, - }, - contact_email: { - name: 'contact_email', - label: 'Contact Email', - type: 'email', - }, - reference_url: { - name: 'reference_url', - label: 'Reference URL', - type: 'url', - help_text: 'Link to external resources or documentation', - }, - }, -}; - -export default function ExampleObjectForm() { - const [isSubmitting, setIsSubmitting] = useState(false); - const [submittedData, setSubmittedData] = useState(null); - - // Example: Editing an existing task - const initialValues = { - title: 'Implement ObjectForm component', - description: 'Create a metadata-driven form component similar to ObjectGridTable', - status: 'in_progress', - priority: 'high', - is_completed: false, - progress: 75, - estimated_hours: 8, - budget: 2000, - contact_email: 'developer@example.com', - reference_url: 'https://github.com/objectql/objectos', - }; - - const handleSubmit = async (data: Record) => { - setIsSubmitting(true); - - // Simulate API call - await new Promise(resolve => setTimeout(resolve, 1000)); - - console.log('Form submitted:', data); - setSubmittedData(data); - setIsSubmitting(false); - }; - - const handleCancel = () => { - console.log('Form cancelled'); - }; - - return ( -
-
-

ObjectForm Example

-

- This form is automatically generated from ObjectQL metadata. - All fields are validated based on their type and configuration. -

-
- -
-

Edit Task

- -
- - {submittedData && ( -
-

Submitted Data

-
-            {JSON.stringify(submittedData, null, 2)}
-          
-
- )} - -
-

Create New Task (Empty Form)

- -
-
- ); -} diff --git a/packages/ui/examples/ObjectGridTableExample.tsx b/packages/ui/examples/ObjectGridTableExample.tsx deleted file mode 100644 index f6d9614c..00000000 --- a/packages/ui/examples/ObjectGridTableExample.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import React from 'react'; -import { ObjectGridTable } from '@objectos/ui'; -import type { ObjectConfig } from '@objectql/types'; - -/** - * Example usage of ObjectGridTable component - * This demonstrates how to use the metadata-driven AG Grid table - */ - -// Define object metadata -const taskObjectConfig: ObjectConfig = { - name: 'task', - label: 'Task', - description: 'Task management object', - fields: { - _id: { - name: '_id', - label: 'ID', - type: 'text', - readonly: true, - hidden: true, - }, - title: { - name: 'title', - label: 'Title', - type: 'text', - required: true, - }, - description: { - name: 'description', - label: 'Description', - type: 'textarea', - }, - status: { - name: 'status', - label: 'Status', - type: 'select', - options: [ - { label: 'To Do', value: 'todo' }, - { label: 'In Progress', value: 'in_progress' }, - { label: 'Done', value: 'done' }, - ], - defaultValue: 'todo', - }, - priority: { - name: 'priority', - label: 'Priority', - type: 'select', - options: [ - { label: 'Low', value: 'low' }, - { label: 'Medium', value: 'medium' }, - { label: 'High', value: 'high' }, - ], - }, - assignee: { - name: 'assignee', - label: 'Assignee', - type: 'lookup', - reference_to: 'user', - }, - is_completed: { - name: 'is_completed', - label: 'Completed', - type: 'boolean', - defaultValue: false, - }, - due_date: { - name: 'due_date', - label: 'Due Date', - type: 'date', - }, - created_at: { - name: 'created_at', - label: 'Created At', - type: 'datetime', - readonly: true, - }, - progress: { - name: 'progress', - label: 'Progress', - type: 'percent', - min: 0, - max: 100, - }, - budget: { - name: 'budget', - label: 'Budget', - type: 'currency', - }, - email: { - name: 'email', - label: 'Contact Email', - type: 'email', - }, - reference_url: { - name: 'reference_url', - label: 'Reference URL', - type: 'url', - }, - }, -}; - -// Sample data -const sampleData = [ - { - _id: '1', - title: 'Implement AG Grid metadata integration', - description: 'Create ObjectGridTable component that uses ObjectConfig', - status: 'in_progress', - priority: 'high', - assignee: { _id: 'user1', name: 'John Doe' }, - is_completed: false, - due_date: new Date('2026-01-15'), - created_at: new Date('2026-01-10'), - progress: 75, - budget: 5000, - email: 'john@example.com', - reference_url: 'https://github.com/objectql/objectos', - }, - { - _id: '2', - title: 'Write documentation', - description: 'Document the new ObjectGridTable component', - status: 'todo', - priority: 'medium', - assignee: { _id: 'user2', name: 'Jane Smith' }, - is_completed: false, - due_date: new Date('2026-01-20'), - created_at: new Date('2026-01-11'), - progress: 0, - budget: 2000, - email: 'jane@example.com', - reference_url: 'https://docs.objectos.dev', - }, - { - _id: '3', - title: 'Review and test', - description: 'Test all field type renderers', - status: 'done', - priority: 'high', - assignee: { _id: 'user1', name: 'John Doe' }, - is_completed: true, - due_date: new Date('2026-01-12'), - created_at: new Date('2026-01-08'), - progress: 100, - budget: 1500, - email: 'john@example.com', - reference_url: 'https://testing.objectos.dev', - }, -]; - -export default function ExampleObjectGridTable() { - const handleSelectionChanged = (selectedRows: any[]) => { - console.log('Selected rows:', selectedRows); - }; - - return ( -
-

ObjectGridTable Example

-

- This table is automatically generated from ObjectQL metadata. - Each field type is rendered with an appropriate cell renderer. -

- - -
- ); -} diff --git a/packages/ui/examples/SectionedForm.tsx b/packages/ui/examples/SectionedForm.tsx deleted file mode 100644 index a841a832..00000000 --- a/packages/ui/examples/SectionedForm.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Sectioned Form Example - * - * This example demonstrates using FormSection to organize - * form fields into logical groups. - */ - -import React from 'react'; -import { DynamicForm } from '@objectos/ui'; -import type { ObjectConfig } from '@objectql/types'; -import { User, Briefcase, Shield, Settings } from 'lucide-react'; -import type { FormSectionConfig } from '@objectos/ui'; - -const employeeConfig: ObjectConfig = { - name: 'employee', - label: 'Employee', - fields: { - // Personal Info - firstName: { - name: 'firstName', - label: 'First Name', - type: 'text', - required: true, - }, - lastName: { - name: 'lastName', - label: 'Last Name', - type: 'text', - required: true, - }, - email: { - name: 'email', - label: 'Email', - type: 'email', - required: true, - }, - phone: { - name: 'phone', - label: 'Phone', - type: 'phone', - }, - - // Employment Info - employeeId: { - name: 'employeeId', - label: 'Employee ID', - type: 'text', - required: true, - }, - department: { - name: 'department', - label: 'Department', - type: 'select', - options: [ - { label: 'Engineering', value: 'engineering' }, - { label: 'Sales', value: 'sales' }, - { label: 'Marketing', value: 'marketing' }, - { label: 'HR', value: 'hr' }, - ], - required: true, - }, - jobTitle: { - name: 'jobTitle', - label: 'Job Title', - type: 'text', - required: true, - }, - startDate: { - name: 'startDate', - label: 'Start Date', - type: 'date', - required: true, - }, - - // Security - username: { - name: 'username', - label: 'Username', - type: 'text', - required: true, - min_length: 3, - max_length: 20, - }, - password: { - name: 'password', - label: 'Password', - type: 'password', - required: true, - min_length: 8, - }, - - // Preferences - timezone: { - name: 'timezone', - label: 'Timezone', - type: 'select', - options: [ - { label: 'UTC', value: 'UTC' }, - { label: 'EST', value: 'America/New_York' }, - { label: 'PST', value: 'America/Los_Angeles' }, - ], - }, - language: { - name: 'language', - label: 'Language', - type: 'select', - options: [ - { label: 'English', value: 'en' }, - { label: 'Spanish', value: 'es' }, - { label: 'French', value: 'fr' }, - ], - }, - }, -}; - -const sections: FormSectionConfig[] = [ - { - id: 'personal', - title: 'Personal Information', - description: 'Basic employee details', - icon: User, - fields: ['firstName', 'lastName', 'email', 'phone'], - collapsible: false, - }, - { - id: 'employment', - title: 'Employment Details', - description: 'Job and department information', - icon: Briefcase, - fields: ['employeeId', 'department', 'jobTitle', 'startDate'], - collapsible: true, - defaultCollapsed: false, - }, - { - id: 'security', - title: 'Security Settings', - description: 'Login credentials', - icon: Shield, - fields: ['username', 'password'], - collapsible: true, - defaultCollapsed: true, - columns: 1, - }, - { - id: 'preferences', - title: 'User Preferences', - icon: Settings, - fields: ['timezone', 'language'], - collapsible: true, - defaultCollapsed: true, - }, -]; - -export function SectionedFormExample() { - const handleSubmit = async (data: any) => { - console.log('Employee data:', data); - await new Promise(resolve => setTimeout(resolve, 1000)); - alert('Employee created!'); - }; - - const handleCancel = () => { - console.log('Cancelled'); - }; - - return ( -
-

New Employee Registration

- - -
- ); -} diff --git a/packages/ui/package.json b/packages/ui/package.json deleted file mode 100644 index 121df686..00000000 --- a/packages/ui/package.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "name": "@objectos/ui", - "version": "0.1.0", - "private": false, - "license": "AGPL-3.0", - "main": "dist/index.js", - "module": "dist/index.mjs", - "types": "dist/index.d.ts", - "files": [ - "dist" - ], - "scripts": { - "build": "npm run build:css && tsup", - "build:css": "postcss src/styles.css -o dist/index.css", - "dev": "tsup --watch", - "lint": "eslint src/**", - "test": "vitest run", - "test:watch": "vitest", - "test:ui": "vitest --ui", - "test:coverage": "vitest run --coverage" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4.1.18", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.1", - "@types/react": "^19.0.11", - "@types/react-dom": "^19.0.5", - "jsdom": "^27.4.0", - "postcss": "^8.4.0", - "postcss-cli": "^11.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "tailwindcss": "^4.1.18", - "tsup": "^8.0.0", - "typescript": "^5.0.0", - "vitest": "^4.0.16" - }, - "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/modifiers": "^9.0.0", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@hookform/resolvers": "^5.2.2", - "@objectql/types": "^3.0.1", - "@radix-ui/react-accordion": "^1.2.12", - "@radix-ui/react-alert-dialog": "^1.1.15", - "@radix-ui/react-aspect-ratio": "^1.1.8", - "@radix-ui/react-avatar": "^1.1.11", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-context-menu": "^2.2.16", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-hover-card": "^1.1.15", - "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-menubar": "^1.1.16", - "@radix-ui/react-navigation-menu": "^1.2.14", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-progress": "^1.1.8", - "@radix-ui/react-radio-group": "^1.3.8", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.8", - "@radix-ui/react-slider": "^1.3.6", - "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-switch": "^1.2.6", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-toggle": "^1.1.10", - "@radix-ui/react-toggle-group": "^1.1.11", - "@radix-ui/react-tooltip": "^1.2.8", - "@tanstack/react-table": "^8.21.3", - "ag-grid-community": "^35.0.0", - "ag-grid-react": "^35.0.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "date-fns": "^4.1.0", - "embla-carousel-react": "^8.6.0", - "input-otp": "^1.4.2", - "lucide-react": "^0.562.0", - "next-themes": "^0.4.6", - "react-day-picker": "^9.13.0", - "react-hook-form": "^7.70.0", - "react-resizable-panels": "^4.3.3", - "recharts": "^2.15.4", - "sonner": "^2.0.7", - "tailwind-merge": "^3.4.0", - "tailwindcss-animate": "^1.0.7", - "vaul": "^1.1.2", - "zod": "^4.3.5" - } -} diff --git a/packages/ui/postcss.config.js b/packages/ui/postcss.config.js deleted file mode 100644 index 52b9b4ba..00000000 --- a/packages/ui/postcss.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - plugins: { - '@tailwindcss/postcss': {}, - }, -} diff --git a/packages/ui/src/components/__tests__/FormActions.test.tsx b/packages/ui/src/components/__tests__/FormActions.test.tsx deleted file mode 100644 index 1012a17e..00000000 --- a/packages/ui/src/components/__tests__/FormActions.test.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react' -import '@testing-library/jest-dom' -import { FormActions } from '../forms/FormActions' -import { describe, it, expect, vi } from 'vitest' - -describe('FormActions', () => { - it('renders save button by default', () => { - const onSave = vi.fn() - render() - - expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument() - }) - - it('calls onSave when save button is clicked', () => { - const onSave = vi.fn() - render() - - fireEvent.click(screen.getByRole('button', { name: 'Save' })) - expect(onSave).toHaveBeenCalledTimes(1) - }) - - it('renders cancel button when onCancel is provided', () => { - const onCancel = vi.fn() - render() - - expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument() - }) - - it('calls onCancel when cancel button is clicked', () => { - const onCancel = vi.fn() - render() - - fireEvent.click(screen.getByRole('button', { name: 'Cancel' })) - expect(onCancel).toHaveBeenCalledTimes(1) - }) - - it('renders Save & New button when onSaveAndNew is provided', () => { - const onSaveAndNew = vi.fn() - render() - - expect(screen.getByRole('button', { name: 'Save & New' })).toBeInTheDocument() - }) - - it('calls onSaveAndNew when Save & New button is clicked', () => { - const onSaveAndNew = vi.fn() - render() - - fireEvent.click(screen.getByRole('button', { name: 'Save & New' })) - expect(onSaveAndNew).toHaveBeenCalledTimes(1) - }) - - it('hides cancel button when hideCancel is true', () => { - render() - - expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument() - }) - - it('hides Save & New button when hideSaveAndNew is true', () => { - render() - - expect(screen.queryByRole('button', { name: 'Save & New' })).not.toBeInTheDocument() - }) - - it('shows loading state when isSubmitting is true', () => { - render() - - expect(screen.getByText('Saving...')).toBeInTheDocument() - }) - - it('disables all buttons when isSubmitting is true', () => { - render( - - ) - - const buttons = screen.getAllByRole('button') - buttons.forEach(button => { - expect(button).toBeDisabled() - }) - }) - - it('uses custom button text when provided', () => { - render( - - ) - - expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'Submit & Create Another' })).toBeInTheDocument() - }) - - it('shows validation error count when errorCount is provided', () => { - render() - - expect(screen.getByText('Please fix 3 errors before saving')).toBeInTheDocument() - }) - - it('shows singular error message when errorCount is 1', () => { - render() - - expect(screen.getByText('Please fix 1 error before saving')).toBeInTheDocument() - }) - - it('disables save buttons when there are errors', () => { - render() - - const saveButton = screen.getByRole('button', { name: 'Save' }) - const saveAndNewButton = screen.getByRole('button', { name: 'Save & New' }) - - expect(saveButton).toBeDisabled() - expect(saveAndNewButton).toBeDisabled() - }) - - it('does not disable cancel button when there are errors', () => { - render() - - const cancelButton = screen.getByRole('button', { name: 'Cancel' }) - expect(cancelButton).not.toBeDisabled() - }) - - it('aligns buttons to the left when align="left"', () => { - const { container } = render() - - expect(container.querySelector('.justify-start')).toBeInTheDocument() - }) - - it('aligns buttons to the center when align="center"', () => { - const { container } = render() - - expect(container.querySelector('.justify-center')).toBeInTheDocument() - }) - - it('aligns buttons to the right by default', () => { - const { container } = render() - - expect(container.querySelector('.justify-end')).toBeInTheDocument() - }) -}) diff --git a/packages/ui/src/components/__tests__/FormSection.test.tsx b/packages/ui/src/components/__tests__/FormSection.test.tsx deleted file mode 100644 index 646d0539..00000000 --- a/packages/ui/src/components/__tests__/FormSection.test.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { render, screen } from '@testing-library/react' -import '@testing-library/jest-dom' -import { FormSection } from '../forms/FormSection' -import { describe, it, expect } from 'vitest' -import { User } from 'lucide-react' - -describe('FormSection', () => { - it('renders section with title', () => { - render( - -
Test content
-
- ) - - expect(screen.getByText('Personal Information')).toBeInTheDocument() - expect(screen.getByText('Test content')).toBeInTheDocument() - }) - - it('renders section with description', () => { - render( - -
Test content
-
- ) - - expect(screen.getByText('Personal Information')).toBeInTheDocument() - expect(screen.getByText('Enter your personal details')).toBeInTheDocument() - }) - - it('renders section with icon', () => { - const { container } = render( - -
Test content
-
- ) - - // Check for icon by class - expect(container.querySelector('svg')).toBeInTheDocument() - }) - - it('renders non-collapsible section by default', () => { - render( - -
Test content
-
- ) - - // Content should be visible immediately - expect(screen.getByText('Test content')).toBeVisible() - - // Should not have collapse trigger button - const buttons = screen.queryAllByRole('button') - expect(buttons.length).toBe(0) - }) - - it('renders collapsible section when collapsible=true', () => { - render( - -
Test content
-
- ) - - // Should have a button for collapsing - const button = screen.getByRole('button') - expect(button).toBeInTheDocument() - }) - - it('starts expanded when collapsible but not defaultCollapsed', () => { - render( - -
Test content
-
- ) - - // Content should be visible - expect(screen.getByText('Test content')).toBeInTheDocument() - }) - - it('starts collapsed when defaultCollapsed=true', () => { - render( - -
Test content
-
- ) - - // Component should render - testing Radix Collapsible's internal state is complex in JSDOM - expect(screen.getByText('Personal Information')).toBeInTheDocument() - }) - - it('applies single column layout when columns=1', () => { - const { container } = render( - -
Test content
-
- ) - - // Check for grid-cols-1 class - const grid = container.querySelector('.grid-cols-1') - expect(grid).toBeInTheDocument() - }) - - it('applies two column layout when columns=2', () => { - const { container } = render( - -
Test content
-
- ) - - // Check for md:grid-cols-2 class - const grid = container.querySelector('.md\\:grid-cols-2') - expect(grid).toBeInTheDocument() - }) - - it('applies custom className', () => { - const { container } = render( - -
Test content
-
- ) - - expect(container.querySelector('.custom-class')).toBeInTheDocument() - }) -}) diff --git a/packages/ui/src/components/app-sidebar.tsx b/packages/ui/src/components/app-sidebar.tsx deleted file mode 100644 index 7b4576b9..00000000 --- a/packages/ui/src/components/app-sidebar.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import * as React from "react" -import { - ArrowUpCircleIcon, - BarChartIcon, - CameraIcon, - ClipboardListIcon, - DatabaseIcon, - FileCodeIcon, - FileIcon, - FileTextIcon, - FolderIcon, - HelpCircleIcon, - LayoutDashboardIcon, - ListIcon, - SearchIcon, - SettingsIcon, - UsersIcon, -} from "lucide-react" - -import { NavDocuments } from "@/components/nav-documents" -import { NavMain } from "@/components/nav-main" -import { NavSecondary } from "@/components/nav-secondary" -import { NavUser } from "@/components/nav-user" -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar" - -const data = { - user: { - name: "shadcn", - email: "m@example.com", - avatar: "/avatars/shadcn.jpg", - }, - navMain: [ - { - title: "Dashboard", - url: "#", - icon: LayoutDashboardIcon, - }, - { - title: "Lifecycle", - url: "#", - icon: ListIcon, - }, - { - title: "Analytics", - url: "#", - icon: BarChartIcon, - }, - { - title: "Projects", - url: "#", - icon: FolderIcon, - }, - { - title: "Team", - url: "#", - icon: UsersIcon, - }, - ], - navClouds: [ - { - title: "Capture", - icon: CameraIcon, - isActive: true, - url: "#", - items: [ - { - title: "Active Proposals", - url: "#", - }, - { - title: "Archived", - url: "#", - }, - ], - }, - { - title: "Proposal", - icon: FileTextIcon, - url: "#", - items: [ - { - title: "Active Proposals", - url: "#", - }, - { - title: "Archived", - url: "#", - }, - ], - }, - { - title: "Prompts", - icon: FileCodeIcon, - url: "#", - items: [ - { - title: "Active Proposals", - url: "#", - }, - { - title: "Archived", - url: "#", - }, - ], - }, - ], - navSecondary: [ - { - title: "Settings", - url: "#", - icon: SettingsIcon, - }, - { - title: "Get Help", - url: "#", - icon: HelpCircleIcon, - }, - { - title: "Search", - url: "#", - icon: SearchIcon, - }, - ], - documents: [ - { - name: "Data Library", - url: "#", - icon: DatabaseIcon, - }, - { - name: "Reports", - url: "#", - icon: ClipboardListIcon, - }, - { - name: "Word Assistant", - url: "#", - icon: FileIcon, - }, - ], -} - -export function AppSidebar({ ...props }: React.ComponentProps) { - return ( - - - - - - - - Acme Inc. - - - - - - - - - - - - - - - ) -} diff --git a/packages/ui/src/components/fields/BooleanField.tsx b/packages/ui/src/components/fields/BooleanField.tsx deleted file mode 100644 index 00b4118b..00000000 --- a/packages/ui/src/components/fields/BooleanField.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from "react" -import { Switch } from "../ui/switch" -import { Checkbox } from "../ui/checkbox" -import { Label } from "../ui/label" -import { cn } from "@/lib/utils" -import { FieldProps } from "./types" - -export interface BooleanFieldProps extends FieldProps { - variant?: "switch" | "checkbox" -} - -export function BooleanField({ - value, - onChange, - disabled, - readOnly, - className, - error, - label, - required, - description, - name, - variant = "switch", -}: BooleanFieldProps) { - - if (variant === "checkbox") { - return ( -
-
- onChange?.(checked === true)} - disabled={disabled || readOnly} - className={cn(error && "border-destructive")} - /> - -
- - {description && !error && ( -

{description}

- )} - {error &&

{error}

} -
- ) - } - - // Default Switch - return ( -
-
- - {description && ( -

{description}

- )} - {error &&

{error}

} -
- -
- ) -} diff --git a/packages/ui/src/components/fields/DateField.tsx b/packages/ui/src/components/fields/DateField.tsx deleted file mode 100644 index 77d7ad46..00000000 --- a/packages/ui/src/components/fields/DateField.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import * as React from "react" -import { format } from "date-fns" -import { Calendar as CalendarIcon, X } from "lucide-react" - -import { cn } from "@/lib/utils" -import { Button } from "../ui/button" -import { Calendar } from "../ui/calendar" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "../ui/popover" -import { Label } from "../ui/label" -import { FieldProps } from "./types" - -export function DateField({ - value, - onChange, - disabled, - readOnly, - className, - placeholder, - error, - label, - required, - description, - name, -}: FieldProps) { - return ( -
- {label && ( - - )} - - - - - - - - - {description && !error && ( -

{description}

- )} - {error &&

{error}

} -
- ) -} diff --git a/packages/ui/src/components/fields/Field.tsx b/packages/ui/src/components/fields/Field.tsx deleted file mode 100644 index d7d5e437..00000000 --- a/packages/ui/src/components/fields/Field.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react' -import { FieldProps } from './types' -import { TextField } from './TextField' -import { NumberField } from './NumberField' -import { BooleanField } from './BooleanField' -import { SelectField } from './SelectField' -import { DateField } from './DateField' -import { TextAreaField } from './TextAreaField' -import { LookupField } from './LookupField' - -export interface GenericFieldProps extends FieldProps { - type: string - referenceTo?: string -} - -export function Field({ type, referenceTo, ...props }: GenericFieldProps) { - switch (type) { - case 'text': - - case 'string': - case 'email': - case 'url': - case 'password': - case 'tel': - return - case 'number': - case 'integer': - case 'float': - case 'currency': - case 'percent': - return - case 'boolean': - return - case 'date': - case 'datetime': - return - case 'select': - return - case 'textarea': - case 'longtext': - return - case 'lookup': - case 'master_detail': - if (referenceTo) { - return - } - return - default: - return - } -} diff --git a/packages/ui/src/components/fields/LookupField.tsx b/packages/ui/src/components/fields/LookupField.tsx deleted file mode 100644 index 6f838eee..00000000 --- a/packages/ui/src/components/fields/LookupField.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import * as React from "react" -import { Check, ChevronsUpDown, Loader2, X } from "lucide-react" - -import { cn } from "@/lib/utils" -import { Button } from "../ui/button" -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "../ui/command" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "../ui/popover" -import { Label } from "../ui/label" -import { LookupFieldProps } from "./types" -import { useDebounce } from "../../hooks/use-debounce" - -export function LookupField({ - value, - onChange, - disabled, - readOnly, - className, - placeholder, - error, - label, - required, - description, - name, - referenceTo, -}: LookupFieldProps) { - const [open, setOpen] = React.useState(false) - const [items, setItems] = React.useState([]) - const [loading, setLoading] = React.useState(false) - const [contentLabel, setContentLabel] = React.useState("") - const [search, setSearch] = React.useState("") - - const debouncedSearch = useDebounce(search, 300) - - // Fetch initial label - React.useEffect(() => { - if (value && !contentLabel) { - if (typeof value === 'object' && (value as any).name) { - setContentLabel((value as any).name || (value as any).title || (value as any)._id); - return; - } - - if (typeof value !== 'string') return; - - // TODO: Use a proper data fetching client - fetch(`/api/data/${referenceTo}/${value}`) - .then(res => res.json()) - .then(data => { - if (data) { - setContentLabel(data.name || data.title || data.email || data._id || value); - } - }) - .catch(() => setContentLabel(value)); - } - }, [value, referenceTo, contentLabel]); - - // Search items - React.useEffect(() => { - if (!open) return; - - setLoading(true); - const params = new URLSearchParams(); - - if (debouncedSearch) { - const filter = JSON.stringify([['name', 'contains', debouncedSearch], 'or', ['title', 'contains', debouncedSearch]]); - params.append('filters', filter); - } - - params.append('top', '20'); - - fetch(`/api/data/${referenceTo}?${params.toString()}`) - .then(res => res.json()) - .then(data => { - const list = Array.isArray(data) ? data : (data.list || []); - setItems(list); - }) - .catch(console.error) - .finally(() => setLoading(false)); - - }, [open, debouncedSearch, referenceTo]); - - const handleSelect = (itemId: string, item: any) => { - onChange?.(itemId === value ? undefined : itemId) - setContentLabel(item.name || item.title || item.email || item._id); - setOpen(false) - } - - const handleClear = (e: React.MouseEvent) => { - e.stopPropagation(); - onChange?.(undefined); - setContentLabel(""); - } - - return ( -
- {label && ( - - )} - - - - - - - - - {loading &&
Loading...
} - - {!loading && items.length === 0 && ( - No results found. - )} - - {!loading && items.length > 0 && ( - - {items.map((item) => { - const itemId = item._id || item.id; - const itemLabel = item.name || item.title || item.email || itemId; - return ( - handleSelect(itemId, item)} - > - - {itemLabel} - - ) - })} - - )} -
-
-
-
- {description && !error && ( -

{description}

- )} - {error &&

{error}

} -
- ) -} diff --git a/packages/ui/src/components/fields/NumberField.tsx b/packages/ui/src/components/fields/NumberField.tsx deleted file mode 100644 index d59becc9..00000000 --- a/packages/ui/src/components/fields/NumberField.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import * as React from "react" -import { cn } from "@/lib/utils" -import { Input } from "../ui/input" -import { Label } from "../ui/label" -import { FieldProps } from "./types" - -export function NumberField({ - value, - onChange, - disabled, - readOnly, - className, - placeholder, - error, - label, - required, - description, - name, -}: FieldProps) { - return ( -
- {label && ( - - )} - { - const val = e.target.value; - onChange?.(val === "" ? undefined : Number(val)); - }} - disabled={disabled} - readOnly={readOnly} - placeholder={placeholder} - className={cn( - error && "border-destructive focus-visible:ring-destructive" - )} - /> - {description && !error && ( -

{description}

- )} - {error &&

{error}

} -
- ) -} diff --git a/packages/ui/src/components/fields/SelectField.tsx b/packages/ui/src/components/fields/SelectField.tsx deleted file mode 100644 index f2678076..00000000 --- a/packages/ui/src/components/fields/SelectField.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from "react" -import { cn } from "@/lib/utils" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "../ui/select" -import { Label } from "../ui/label" -import { FieldProps } from "./types" - -export function SelectField({ - value, - onChange, - disabled, - readOnly, - className, - placeholder, - error, - label, - required, - description, - name, - options = [], -}: FieldProps) { - - // Normalize options - const normalizedOptions = React.useMemo(() => { - return options.map((opt) => { - if (typeof opt === 'string') return { label: opt, value: opt }; - return opt; - }); - }, [options]); - - return ( -
- {label && ( - - )} - - {description && !error && ( -

{description}

- )} - {error &&

{error}

} -
- ) -} diff --git a/packages/ui/src/components/fields/TextAreaField.tsx b/packages/ui/src/components/fields/TextAreaField.tsx deleted file mode 100644 index 1ed59af8..00000000 --- a/packages/ui/src/components/fields/TextAreaField.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from "react" -import { cn } from "@/lib/utils" -import { Textarea } from "../ui/textarea" -import { Label } from "../ui/label" -import { FieldProps } from "./types" - -export function TextAreaField({ - value, - onChange, - disabled, - readOnly, - className, - placeholder, - error, - label, - required, - description, - name, - rows = 3, -}: FieldProps & { rows?: number }) { - return ( -
- {label && ( - - )} -