From a0a576ec50538df7881bb2a5841e11c091f721b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:30:46 +0000 Subject: [PATCH 1/5] Initial plan From ddbed065f0e1255e47513bcc380a6cb48f29ed46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:34:18 +0000 Subject: [PATCH 2/5] Refactor field renderers and ObjectGrid to use Shadcn components Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/views/src/ObjectGrid.tsx | 42 +++++--- packages/views/src/field-renderers.tsx | 133 +++++++++++++++---------- 2 files changed, 109 insertions(+), 66 deletions(-) diff --git a/packages/views/src/ObjectGrid.tsx b/packages/views/src/ObjectGrid.tsx index 7ccfc2b2..0eff63c7 100644 --- a/packages/views/src/ObjectGrid.tsx +++ b/packages/views/src/ObjectGrid.tsx @@ -25,6 +25,14 @@ import React, { useEffect, useState, useCallback } from 'react'; import type { ObjectGridSchema, DataSource, ListColumn, ViewData } from '@object-ui/types'; import { SchemaRenderer } from '@object-ui/react'; import { getCellRenderer } from './field-renderers'; +import { Button } from '@object-ui/components'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@object-ui/components'; +import { Edit, Trash2, MoreVertical } from 'lucide-react'; export interface ObjectGridProps { schema: ObjectGridSchema; @@ -298,18 +306,28 @@ export const ObjectGrid: React.FC = ({ header: 'Actions', accessorKey: '_actions', cell: (_value: any, row: any) => ( -
- {operations?.update && onEdit && ( - - )} - {operations?.delete && onDelete && ( - - )} -
+ + + + + + {operations?.update && onEdit && ( + onEdit(row)}> + + Edit + + )} + {operations?.delete && onDelete && ( + onDelete(row)}> + + Delete + + )} + + ), sortable: false, }, diff --git a/packages/views/src/field-renderers.tsx b/packages/views/src/field-renderers.tsx index 3fe01826..08016207 100644 --- a/packages/views/src/field-renderers.tsx +++ b/packages/views/src/field-renderers.tsx @@ -15,6 +15,10 @@ import React from 'react'; import type { FieldMetadata, SelectOptionMetadata } from '@object-ui/types'; +import { Badge } from '@object-ui/components'; +import { Avatar, AvatarFallback } from '@object-ui/components'; +import { Button } from '@object-ui/components'; +import { Check, X } from 'lucide-react'; /** * Cell renderer props @@ -133,13 +137,15 @@ export function BooleanCellRenderer({ value }: CellRendererProps): React.ReactEl return (
{value ? ( - - ✓ - + + + True + ) : ( - - ✗ - + + + False + )}
); @@ -173,17 +179,20 @@ export function SelectCellRenderer({ value, field }: CellRendererProps): React.R if (!value) return -; - // Color mapping for Tailwind CSS (to avoid dynamic class names) - const colorClasses: Record = { - gray: { bg: 'bg-gray-100', text: 'text-gray-800' }, - red: { bg: 'bg-red-100', text: 'text-red-800' }, - orange: { bg: 'bg-orange-100', text: 'text-orange-800' }, - yellow: { bg: 'bg-yellow-100', text: 'text-yellow-800' }, - green: { bg: 'bg-green-100', text: 'text-green-800' }, - blue: { bg: 'bg-blue-100', text: 'text-blue-800' }, - indigo: { bg: 'bg-indigo-100', text: 'text-indigo-800' }, - purple: { bg: 'bg-purple-100', text: 'text-purple-800' }, - pink: { bg: 'bg-pink-100', text: 'text-pink-800' }, + // Color to Tailwind class mapping for custom Badge styling + const getColorClasses = (color?: string) => { + const colorMap: Record = { + gray: 'bg-gray-100 text-gray-800 border-gray-300', + red: 'bg-red-100 text-red-800 border-red-300', + orange: 'bg-orange-100 text-orange-800 border-orange-300', + yellow: 'bg-yellow-100 text-yellow-800 border-yellow-300', + green: 'bg-green-100 text-green-800 border-green-300', + blue: 'bg-blue-100 text-blue-800 border-blue-300', + indigo: 'bg-indigo-100 text-indigo-800 border-indigo-300', + purple: 'bg-purple-100 text-purple-800 border-purple-300', + pink: 'bg-pink-100 text-pink-800 border-pink-300', + }; + return colorMap[color || 'blue'] || colorMap.blue; }; // Handle multiple values @@ -193,16 +202,16 @@ export function SelectCellRenderer({ value, field }: CellRendererProps): React.R {value.map((val, idx) => { const option = options.find(opt => opt.value === val); const label = option?.label || val; - const color = option?.color || 'gray'; - const classes = colorClasses[color] || colorClasses.gray; + const colorClasses = getColorClasses(option?.color); return ( - {label} - + ); })} @@ -212,15 +221,15 @@ export function SelectCellRenderer({ value, field }: CellRendererProps): React.R // Handle single value const option = options.find(opt => opt.value === value); const label = option?.label || value; - const color = option?.color || 'blue'; - const classes = colorClasses[color] || colorClasses.blue; + const colorClasses = getColorClasses(option?.color); return ( - {label} - + ); } @@ -231,13 +240,18 @@ export function EmailCellRenderer({ value }: CellRendererProps): React.ReactElem if (!value) return -; return ( - e.stopPropagation()} + ); } @@ -248,15 +262,20 @@ export function UrlCellRenderer({ value }: CellRendererProps): React.ReactElemen if (!value) return -; return ( - e.stopPropagation()} + ); } @@ -313,11 +332,11 @@ export function ImageCellRenderer({ value }: CellRendererProps): React.ReactElem key={idx} src={img.url || ''} alt={img.name || `Image ${idx + 1}`} - className="w-8 h-8 rounded border-2 border-white object-cover" + className="size-8 rounded-md border-2 border-white object-cover" /> ))} {value.length > 3 && ( -
+
+{value.length - 3}
)} @@ -329,7 +348,7 @@ export function ImageCellRenderer({ value }: CellRendererProps): React.ReactElem {value.name ); } @@ -391,19 +410,23 @@ export function UserCellRenderer({ value }: CellRendererProps): React.ReactEleme const initials = name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2); return ( -
- {initials} -
+ + {initials} + + ); })} {value.length > 3 && ( -
- +{value.length - 3} -
+ + + +{value.length - 3} + + )}
); @@ -414,9 +437,11 @@ export function UserCellRenderer({ value }: CellRendererProps): React.ReactEleme return (
-
- {initials} -
+ + + {initials} + + {name}
); From 6bde96ddad2b838c282ae79b9b3746abccde5c9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:37:28 +0000 Subject: [PATCH 3/5] Update field-renderers tests to match new Shadcn components Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/views/src/__tests__/field-renderers.test.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/views/src/__tests__/field-renderers.test.tsx b/packages/views/src/__tests__/field-renderers.test.tsx index 56570683..2957b07e 100644 --- a/packages/views/src/__tests__/field-renderers.test.tsx +++ b/packages/views/src/__tests__/field-renderers.test.tsx @@ -82,13 +82,13 @@ describe('Field Renderers', () => { it('renders checkmark for true', () => { const field = { type: 'boolean', name: 'active' } as FieldMetadata; const { container } = render(); - expect(container.textContent).toContain('✓'); + expect(container.textContent).toContain('True'); }); it('renders cross for false', () => { const field = { type: 'boolean', name: 'active' } as FieldMetadata; const { container } = render(); - expect(container.textContent).toContain('✗'); + expect(container.textContent).toContain('False'); }); }); @@ -264,7 +264,8 @@ describe('Field Renderers', () => { { name: 'Jane Smith' }, ]; const { container } = render(); - const avatars = container.querySelectorAll('.rounded-full'); + // Avatar component creates nested elements, so we look for the Avatar data-slot attribute instead + const avatars = container.querySelectorAll('[data-slot="avatar"]'); expect(avatars.length).toBe(2); }); }); From ee1863a6fd5a47c301b38c95fd059951678dc16e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:40:01 +0000 Subject: [PATCH 4/5] Extract calendar, gantt, and map views to separate plugin packages Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/plugin-calendar/package.json | 52 +++ .../src/ObjectCalendar.tsx | 0 packages/plugin-calendar/src/index.tsx | 29 ++ packages/plugin-calendar/tsconfig.json | 18 + packages/plugin-calendar/vite.config.ts | 50 +++ packages/plugin-gantt/package.json | 52 +++ .../src/ObjectGantt.tsx | 0 packages/plugin-gantt/src/index.tsx | 29 ++ packages/plugin-gantt/tsconfig.json | 18 + packages/plugin-gantt/vite.config.ts | 50 +++ packages/plugin-map/package.json | 52 +++ .../{views => plugin-map}/src/ObjectMap.tsx | 0 packages/plugin-map/src/index.tsx | 29 ++ packages/plugin-map/tsconfig.json | 18 + packages/plugin-map/vite.config.ts | 50 +++ packages/views/src/ObjectKanban.tsx | 362 ------------------ .../src/__tests__/ObjectCalendar.test.tsx | 167 -------- .../views/src/__tests__/ObjectGantt.test.tsx | 176 --------- .../views/src/__tests__/ObjectKanban.test.tsx | 141 ------- .../views/src/__tests__/ObjectMap.test.tsx | 233 ----------- packages/views/src/index.tsx | 72 ---- 21 files changed, 447 insertions(+), 1151 deletions(-) create mode 100644 packages/plugin-calendar/package.json rename packages/{views => plugin-calendar}/src/ObjectCalendar.tsx (100%) create mode 100644 packages/plugin-calendar/src/index.tsx create mode 100644 packages/plugin-calendar/tsconfig.json create mode 100644 packages/plugin-calendar/vite.config.ts create mode 100644 packages/plugin-gantt/package.json rename packages/{views => plugin-gantt}/src/ObjectGantt.tsx (100%) create mode 100644 packages/plugin-gantt/src/index.tsx create mode 100644 packages/plugin-gantt/tsconfig.json create mode 100644 packages/plugin-gantt/vite.config.ts create mode 100644 packages/plugin-map/package.json rename packages/{views => plugin-map}/src/ObjectMap.tsx (100%) create mode 100644 packages/plugin-map/src/index.tsx create mode 100644 packages/plugin-map/tsconfig.json create mode 100644 packages/plugin-map/vite.config.ts delete mode 100644 packages/views/src/ObjectKanban.tsx delete mode 100644 packages/views/src/__tests__/ObjectCalendar.test.tsx delete mode 100644 packages/views/src/__tests__/ObjectGantt.test.tsx delete mode 100644 packages/views/src/__tests__/ObjectKanban.test.tsx delete mode 100644 packages/views/src/__tests__/ObjectMap.test.tsx diff --git a/packages/plugin-calendar/package.json b/packages/plugin-calendar/package.json new file mode 100644 index 00000000..80e5600d --- /dev/null +++ b/packages/plugin-calendar/package.json @@ -0,0 +1,52 @@ +{ + "name": "@object-ui/plugin-calendar", + "version": "0.3.0", + "type": "module", + "license": "MIT", + "description": "Calendar view plugin for Object UI", + "homepage": "https://www.objectui.org", + "repository": { + "type": "git", + "url": "https://github.com/objectstack-ai/objectui.git", + "directory": "packages/plugin-calendar" + }, + "bugs": { + "url": "https://github.com/objectstack-ai/objectui/issues" + }, + "main": "dist/index.umd.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.umd.cjs" + } + }, + "scripts": { + "build": "vite build", + "test": "vitest run", + "test:watch": "vitest", + "type-check": "tsc --noEmit", + "lint": "eslint ." + }, + "dependencies": { + "@object-ui/components": "workspace:*", + "@object-ui/core": "workspace:*", + "@object-ui/react": "workspace:*", + "@object-ui/types": "workspace:*", + "lucide-react": "^0.563.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vite-plugin-dts": "^4.5.4" + } +} diff --git a/packages/views/src/ObjectCalendar.tsx b/packages/plugin-calendar/src/ObjectCalendar.tsx similarity index 100% rename from packages/views/src/ObjectCalendar.tsx rename to packages/plugin-calendar/src/ObjectCalendar.tsx diff --git a/packages/plugin-calendar/src/index.tsx b/packages/plugin-calendar/src/index.tsx new file mode 100644 index 00000000..a3f77622 --- /dev/null +++ b/packages/plugin-calendar/src/index.tsx @@ -0,0 +1,29 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { ComponentRegistry } from '@object-ui/core'; +import { ObjectCalendar } from './ObjectCalendar'; +import type { ObjectCalendarProps } from './ObjectCalendar'; + +export { ObjectCalendar }; +export type { ObjectCalendarProps }; + +// Register component +const ObjectCalendarRenderer: React.FC<{ schema: any }> = ({ schema }) => { + return ; +}; + +ComponentRegistry.register('object-calendar', ObjectCalendarRenderer, { + label: 'Object Calendar', + category: 'plugin', + inputs: [ + { name: 'objectName', type: 'string', label: 'Object Name', required: true }, + { name: 'calendar', type: 'object', label: 'Calendar Config', description: 'startDateField, endDateField, titleField, colorField' }, + ], +}); diff --git a/packages/plugin-calendar/tsconfig.json b/packages/plugin-calendar/tsconfig.json new file mode 100644 index 00000000..22195355 --- /dev/null +++ b/packages/plugin-calendar/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "noEmit": false, + "declaration": true, + "composite": true, + "declarationMap": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} diff --git a/packages/plugin-calendar/vite.config.ts b/packages/plugin-calendar/vite.config.ts new file mode 100644 index 00000000..229ecc1a --- /dev/null +++ b/packages/plugin-calendar/vite.config.ts @@ -0,0 +1,50 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import dts from 'vite-plugin-dts'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [ + react(), + dts({ + insertTypesEntry: true, + include: ['src'], + exclude: ['**/*.test.ts', '**/*.test.tsx', 'node_modules'], + skipDiagnostics: true, + }), + ], + resolve: { + alias: { + '@': resolve(__dirname, './src'), + }, + }, + build: { + lib: { + entry: resolve(__dirname, 'src/index.tsx'), + name: 'ObjectUIPluginCalendar', + fileName: 'index', + }, + rollupOptions: { + external: ['react', 'react-dom', '@object-ui/components', '@object-ui/core', '@object-ui/react', '@object-ui/types', 'lucide-react'], + output: { + globals: { + react: 'React', + 'react-dom': 'ReactDOM', + '@object-ui/components': 'ObjectUIComponents', + '@object-ui/core': 'ObjectUICore', + '@object-ui/react': 'ObjectUIReact', + '@object-ui/types': 'ObjectUITypes', + 'lucide-react': 'LucideReact', + }, + }, + }, + }, +}); diff --git a/packages/plugin-gantt/package.json b/packages/plugin-gantt/package.json new file mode 100644 index 00000000..54e17215 --- /dev/null +++ b/packages/plugin-gantt/package.json @@ -0,0 +1,52 @@ +{ + "name": "@object-ui/plugin-gantt", + "version": "0.3.0", + "type": "module", + "license": "MIT", + "description": "Gantt chart plugin for Object UI", + "homepage": "https://www.objectui.org", + "repository": { + "type": "git", + "url": "https://github.com/objectstack-ai/objectui.git", + "directory": "packages/plugin-gantt" + }, + "bugs": { + "url": "https://github.com/objectstack-ai/objectui/issues" + }, + "main": "dist/index.umd.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.umd.cjs" + } + }, + "scripts": { + "build": "vite build", + "test": "vitest run", + "test:watch": "vitest", + "type-check": "tsc --noEmit", + "lint": "eslint ." + }, + "dependencies": { + "@object-ui/components": "workspace:*", + "@object-ui/core": "workspace:*", + "@object-ui/react": "workspace:*", + "@object-ui/types": "workspace:*", + "lucide-react": "^0.563.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vite-plugin-dts": "^4.5.4" + } +} diff --git a/packages/views/src/ObjectGantt.tsx b/packages/plugin-gantt/src/ObjectGantt.tsx similarity index 100% rename from packages/views/src/ObjectGantt.tsx rename to packages/plugin-gantt/src/ObjectGantt.tsx diff --git a/packages/plugin-gantt/src/index.tsx b/packages/plugin-gantt/src/index.tsx new file mode 100644 index 00000000..9cfa4c28 --- /dev/null +++ b/packages/plugin-gantt/src/index.tsx @@ -0,0 +1,29 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { ComponentRegistry } from '@object-ui/core'; +import { ObjectGantt } from './ObjectGantt'; +import type { ObjectGanttProps } from './ObjectGantt'; + +export { ObjectGantt }; +export type { ObjectGanttProps }; + +// Register component +const ObjectGanttRenderer: React.FC<{ schema: any }> = ({ schema }) => { + return ; +}; + +ComponentRegistry.register('object-gantt', ObjectGanttRenderer, { + label: 'Object Gantt', + category: 'plugin', + inputs: [ + { name: 'objectName', type: 'string', label: 'Object Name', required: true }, + { name: 'gantt', type: 'object', label: 'Gantt Config', description: 'startDateField, endDateField, titleField, progressField, dependenciesField' }, + ], +}); diff --git a/packages/plugin-gantt/tsconfig.json b/packages/plugin-gantt/tsconfig.json new file mode 100644 index 00000000..22195355 --- /dev/null +++ b/packages/plugin-gantt/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "noEmit": false, + "declaration": true, + "composite": true, + "declarationMap": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} diff --git a/packages/plugin-gantt/vite.config.ts b/packages/plugin-gantt/vite.config.ts new file mode 100644 index 00000000..ef5dd6e2 --- /dev/null +++ b/packages/plugin-gantt/vite.config.ts @@ -0,0 +1,50 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import dts from 'vite-plugin-dts'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [ + react(), + dts({ + insertTypesEntry: true, + include: ['src'], + exclude: ['**/*.test.ts', '**/*.test.tsx', 'node_modules'], + skipDiagnostics: true, + }), + ], + resolve: { + alias: { + '@': resolve(__dirname, './src'), + }, + }, + build: { + lib: { + entry: resolve(__dirname, 'src/index.tsx'), + name: 'ObjectUIPluginGantt', + fileName: 'index', + }, + rollupOptions: { + external: ['react', 'react-dom', '@object-ui/components', '@object-ui/core', '@object-ui/react', '@object-ui/types', 'lucide-react'], + output: { + globals: { + react: 'React', + 'react-dom': 'ReactDOM', + '@object-ui/components': 'ObjectUIComponents', + '@object-ui/core': 'ObjectUICore', + '@object-ui/react': 'ObjectUIReact', + '@object-ui/types': 'ObjectUITypes', + 'lucide-react': 'LucideReact', + }, + }, + }, + }, +}); diff --git a/packages/plugin-map/package.json b/packages/plugin-map/package.json new file mode 100644 index 00000000..08c8f724 --- /dev/null +++ b/packages/plugin-map/package.json @@ -0,0 +1,52 @@ +{ + "name": "@object-ui/plugin-map", + "version": "0.3.0", + "type": "module", + "license": "MIT", + "description": "Map visualization plugin for Object UI", + "homepage": "https://www.objectui.org", + "repository": { + "type": "git", + "url": "https://github.com/objectstack-ai/objectui.git", + "directory": "packages/plugin-map" + }, + "bugs": { + "url": "https://github.com/objectstack-ai/objectui/issues" + }, + "main": "dist/index.umd.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.umd.cjs" + } + }, + "scripts": { + "build": "vite build", + "test": "vitest run", + "test:watch": "vitest", + "type-check": "tsc --noEmit", + "lint": "eslint ." + }, + "dependencies": { + "@object-ui/components": "workspace:*", + "@object-ui/core": "workspace:*", + "@object-ui/react": "workspace:*", + "@object-ui/types": "workspace:*", + "lucide-react": "^0.563.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vite-plugin-dts": "^4.5.4" + } +} diff --git a/packages/views/src/ObjectMap.tsx b/packages/plugin-map/src/ObjectMap.tsx similarity index 100% rename from packages/views/src/ObjectMap.tsx rename to packages/plugin-map/src/ObjectMap.tsx diff --git a/packages/plugin-map/src/index.tsx b/packages/plugin-map/src/index.tsx new file mode 100644 index 00000000..76816db6 --- /dev/null +++ b/packages/plugin-map/src/index.tsx @@ -0,0 +1,29 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { ComponentRegistry } from '@object-ui/core'; +import { ObjectMap } from './ObjectMap'; +import type { ObjectMapProps } from './ObjectMap'; + +export { ObjectMap }; +export type { ObjectMapProps }; + +// Register component +const ObjectMapRenderer: React.FC<{ schema: any }> = ({ schema }) => { + return ; +}; + +ComponentRegistry.register('object-map', ObjectMapRenderer, { + label: 'Object Map', + category: 'plugin', + inputs: [ + { name: 'objectName', type: 'string', label: 'Object Name', required: true }, + { name: 'map', type: 'object', label: 'Map Config', description: 'latitudeField, longitudeField, titleField' }, + ], +}); diff --git a/packages/plugin-map/tsconfig.json b/packages/plugin-map/tsconfig.json new file mode 100644 index 00000000..22195355 --- /dev/null +++ b/packages/plugin-map/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "noEmit": false, + "declaration": true, + "composite": true, + "declarationMap": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} diff --git a/packages/plugin-map/vite.config.ts b/packages/plugin-map/vite.config.ts new file mode 100644 index 00000000..f0aca58f --- /dev/null +++ b/packages/plugin-map/vite.config.ts @@ -0,0 +1,50 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import dts from 'vite-plugin-dts'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [ + react(), + dts({ + insertTypesEntry: true, + include: ['src'], + exclude: ['**/*.test.ts', '**/*.test.tsx', 'node_modules'], + skipDiagnostics: true, + }), + ], + resolve: { + alias: { + '@': resolve(__dirname, './src'), + }, + }, + build: { + lib: { + entry: resolve(__dirname, 'src/index.tsx'), + name: 'ObjectUIPluginMap', + fileName: 'index', + }, + rollupOptions: { + external: ['react', 'react-dom', '@object-ui/components', '@object-ui/core', '@object-ui/react', '@object-ui/types', 'lucide-react'], + output: { + globals: { + react: 'React', + 'react-dom': 'ReactDOM', + '@object-ui/components': 'ObjectUIComponents', + '@object-ui/core': 'ObjectUICore', + '@object-ui/react': 'ObjectUIReact', + '@object-ui/types': 'ObjectUITypes', + 'lucide-react': 'LucideReact', + }, + }, + }, + }, +}); diff --git a/packages/views/src/ObjectKanban.tsx b/packages/views/src/ObjectKanban.tsx deleted file mode 100644 index 1cd3b525..00000000 --- a/packages/views/src/ObjectKanban.tsx +++ /dev/null @@ -1,362 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/** - * ObjectKanban Component - * - * A specialized kanban board component that works with ObjectQL data sources. - * Auto-generates kanban columns from data based on groupByField configuration. - * Implements the kanban view type from @objectstack/spec view.zod ListView schema. - * - * Features: - * - Drag-and-drop kanban board - * - Auto-grouping by field (e.g., status, stage) - * - Column summaries (e.g., sum of amounts) - * - Card customization based on field configuration - * - Works with object/api/value data providers - */ - -import React, { useEffect, useState, useCallback, useMemo } from 'react'; -import type { ObjectGridSchema, DataSource, ViewData, KanbanConfig } from '@object-ui/types'; - -export interface ObjectKanbanProps { - schema: ObjectGridSchema; - dataSource?: DataSource; - className?: string; - onCardClick?: (record: any) => void; - onCardMove?: (cardId: string, fromColumn: string, toColumn: string) => void; - onEdit?: (record: any) => void; - onDelete?: (record: any) => void; -} - -/** - * Helper to get data configuration from schema - */ -function getDataConfig(schema: ObjectGridSchema): ViewData | null { - if (schema.data) { - return schema.data; - } - - if (schema.staticData) { - return { - provider: 'value', - items: schema.staticData, - }; - } - - if (schema.objectName) { - return { - provider: 'object', - object: schema.objectName, - }; - } - - return null; -} - -/** - * Helper to convert sort config to QueryParams format - */ -function convertSortToQueryParams(sort: string | any[] | undefined): Record | undefined { - if (!sort) return undefined; - - // If it's a string like "name desc" - if (typeof sort === 'string') { - const parts = sort.split(' '); - const field = parts[0]; - const order = (parts[1]?.toLowerCase() === 'desc' ? 'desc' : 'asc') as 'asc' | 'desc'; - return { [field]: order }; - } - - // If it's an array of SortConfig objects - if (Array.isArray(sort)) { - return sort.reduce((acc, item) => { - if (item.field && item.order) { - acc[item.field] = item.order; - } - return acc; - }, {} as Record); - } - - return undefined; -} - -/** - * Helper to get kanban configuration from schema - * Extracts kanban-specific settings or provides defaults - */ -function getKanbanConfig(schema: ObjectGridSchema): KanbanConfig | null { - // Check if schema has kanban configuration in filter or separate config - if (schema.filter && typeof schema.filter === 'object' && 'kanban' in schema.filter) { - return (schema.filter as any).kanban as KanbanConfig; - } - - // For backward compatibility, check if schema has kanban config at root - if ((schema as any).kanban) { - return (schema as any).kanban as KanbanConfig; - } - - return null; -} - -export const ObjectKanban: React.FC = ({ - schema, - dataSource, - className, - onCardClick, - onCardMove, -}) => { - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [objectSchema, setObjectSchema] = useState(null); - - const dataConfig = getDataConfig(schema); - const kanbanConfig = getKanbanConfig(schema); - const hasInlineData = dataConfig?.provider === 'value'; - - // Fetch data based on provider - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - - if (hasInlineData && dataConfig?.provider === 'value') { - setData(dataConfig.items as any[]); - setLoading(false); - return; - } - - if (!dataSource) { - throw new Error('DataSource required for object/api providers'); - } - - if (dataConfig?.provider === 'object') { - const objectName = dataConfig.object; - const result = await dataSource.find(objectName, { - $filter: schema.filter, - $orderby: convertSortToQueryParams(schema.sort), - }); - setData(result?.data || []); - } else if (dataConfig?.provider === 'api') { - // For API provider, we'd need to fetch from the read endpoint - // This would typically be handled by a custom hook or data fetching layer - console.warn('API provider not yet implemented for ObjectKanban'); - setData([]); - } - - setLoading(false); - } catch (err) { - setError(err as Error); - setLoading(false); - } - }; - - fetchData(); - }, [dataConfig, dataSource, hasInlineData, schema.filter, schema.sort]); - - // Fetch object schema for field metadata - useEffect(() => { - const fetchObjectSchema = async () => { - try { - if (!dataSource) return; - - const objectName = dataConfig?.provider === 'object' - ? dataConfig.object - : schema.objectName; - - if (!objectName) return; - - const schemaData = await dataSource.getObjectSchema(objectName); - setObjectSchema(schemaData); - } catch (err) { - console.error('Failed to fetch object schema:', err); - } - }; - - if (!hasInlineData && dataSource) { - fetchObjectSchema(); - } - }, [schema.objectName, dataSource, hasInlineData, dataConfig]); - - // Group data into kanban columns - const kanbanColumns = useMemo(() => { - if (!kanbanConfig || !data.length) { - return []; - } - - const groupByField = kanbanConfig.groupByField; - const summarizeField = kanbanConfig.summarizeField; - const displayColumns = kanbanConfig.columns || []; - - // Get unique values for grouping field - const uniqueValues = [...new Set(data.map(record => record[groupByField]))].filter(Boolean); - - // Create columns for each unique value - return uniqueValues.map(value => { - const columnCards = data.filter(record => record[groupByField] === value); - - // Calculate summary if configured - let summary; - if (summarizeField) { - summary = columnCards.reduce((sum, card) => { - const val = card[summarizeField]; - return sum + (typeof val === 'number' ? val : 0); - }, 0); - } - - return { - id: String(value), - title: String(value), - summary, - cards: columnCards.map((record, idx) => ({ - id: record.id || record._id || `${value}-${idx}`, - title: record[displayColumns[0]] || record.name || record.title || 'Untitled', - description: displayColumns[1] ? record[displayColumns[1]] : undefined, - data: record, - // Map additional fields to display - fields: displayColumns.slice(2).reduce((acc: Record, fieldName: string) => { - acc[fieldName] = record[fieldName]; - return acc; - }, {} as Record), - })), - }; - }); - }, [data, kanbanConfig]); - - const handleCardMoveInternal = useCallback((cardId: string, fromColumnId: string, toColumnId: string) => { - if (!kanbanConfig) return; - - // Update the local data - setData(prevData => { - return prevData.map(record => { - const recordId = record.id || record._id; - if (String(recordId) === cardId) { - return { - ...record, - [kanbanConfig.groupByField]: toColumnId, - }; - } - return record; - }); - }); - - // Notify parent - if (onCardMove) { - onCardMove(cardId, fromColumnId, toColumnId); - } - - // If we have a dataSource, persist the change - if (dataSource && dataConfig?.provider === 'object') { - const objectName = dataConfig.object; - dataSource.update(objectName, cardId, { - [kanbanConfig.groupByField]: toColumnId, - }).catch((err: any) => { - console.error('Failed to update record:', err); - // Revert the change on error - setData(prevData => { - return prevData.map(record => { - const recordId = record.id || record._id; - if (String(recordId) === cardId) { - return { - ...record, - [kanbanConfig.groupByField]: fromColumnId, - }; - } - return record; - }); - }); - }); - } - }, [kanbanConfig, dataConfig, dataSource, onCardMove]); - - if (loading) { - return ( -
-
-
Loading kanban board...
-
-
- ); - } - - if (error) { - return ( -
-
-
Error: {error.message}
-
-
- ); - } - - if (!kanbanConfig) { - return ( -
-
-
- Kanban configuration required. Please specify groupByField and columns. -
-
-
- ); - } - - // Render using the kanban plugin component - // We'll use SchemaRenderer to render the kanban component - return ( -
- {/* Placeholder for actual kanban rendering - would use plugin-kanban */} -
-
- {kanbanColumns.map(column => ( -
-
-

{column.title}

- {column.summary !== undefined && ( - - {column.summary} - - )} -
-
- {column.cards.map(card => ( -
onCardClick?.(card.data)} - > -

{card.title}

- {card.description && ( -

- {card.description} -

- )} - {Object.keys(card.fields).length > 0 && ( -
- {Object.entries(card.fields).map(([key, value]) => ( -
- {key}:{' '} - {String(value)} -
- ))} -
- )} -
- ))} -
-
- ))} -
-
-
- ); -}; diff --git a/packages/views/src/__tests__/ObjectCalendar.test.tsx b/packages/views/src/__tests__/ObjectCalendar.test.tsx deleted file mode 100644 index fd53d284..00000000 --- a/packages/views/src/__tests__/ObjectCalendar.test.tsx +++ /dev/null @@ -1,167 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/react'; -import { ObjectCalendar } from '../ObjectCalendar'; -import type { ObjectGridSchema, DataSource } from '@object-ui/types'; - -describe('ObjectCalendar', () => { - let mockDataSource: DataSource; - let mockSchema: ObjectGridSchema; - - beforeEach(() => { - // Mock data source - mockDataSource = { - find: vi.fn().mockResolvedValue({ - data: [ - { - _id: '1', - title: 'Meeting 1', - start_date: '2026-01-15T10:00:00Z', - end_date: '2026-01-15T11:00:00Z', - }, - { - _id: '2', - title: 'Meeting 2', - start_date: '2026-01-20T14:00:00Z', - end_date: '2026-01-20T15:00:00Z', - }, - ], - }), - getObjectSchema: vi.fn().mockResolvedValue({ - name: 'events', - label: 'Events', - fields: { - title: { type: 'text', label: 'Title' }, - start_date: { type: 'datetime', label: 'Start Date' }, - end_date: { type: 'datetime', label: 'End Date' }, - }, - }), - } as any; - - // Mock schema with calendar config - mockSchema = { - type: 'object-grid', - objectName: 'events', - data: { - provider: 'object', - object: 'events', - }, - filter: { - calendar: { - startDateField: 'start_date', - endDateField: 'end_date', - titleField: 'title', - }, - }, - } as any; - }); - - it('should render loading state initially', () => { - render(); - expect(screen.getByText(/Loading calendar/i)).toBeInTheDocument(); - }); - - it('should fetch and display calendar data', async () => { - render(); - - await waitFor(() => { - expect(mockDataSource.find).toHaveBeenCalledWith('events', expect.any(Object)); - }); - - await waitFor(() => { - expect(screen.getByText('Meeting 1')).toBeInTheDocument(); - expect(screen.getByText('Meeting 2')).toBeInTheDocument(); - }); - }); - - it('should render with inline data', async () => { - // Test inline data with the same structure as the mock data - const inlineDataSchema: ObjectGridSchema = { - ...mockSchema, - data: { - provider: 'value', - items: [ - { - _id: '1', - title: 'Event A', - start_date: '2026-01-15T10:00:00Z', - end_date: '2026-01-15T11:00:00Z', - }, - ], - }, - } as any; - - // Don't pass dataSource for inline data - render(); - - await waitFor(() => { - expect(screen.getByText('Event A')).toBeInTheDocument(); - }, { timeout: 3000 }); - }); - - it('should show error when calendar config is missing', async () => { - const schemaWithoutCalendar = { - ...mockSchema, - filter: {}, - } as any; - - render(); - - await waitFor(() => { - expect(screen.getByText(/Calendar configuration required/i)).toBeInTheDocument(); - }); - }); - - it('should display month navigation', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText('Previous')).toBeInTheDocument(); - expect(screen.getByText('Today')).toBeInTheDocument(); - expect(screen.getByText('Next')).toBeInTheDocument(); - }); - }); - - it('should handle event click', async () => { - const onEventClick = vi.fn(); - render( - - ); - - await waitFor(() => { - expect(screen.getByText('Meeting 1')).toBeInTheDocument(); - }); - - const event = screen.getByText('Meeting 1'); - event.click(); - - expect(onEventClick).toHaveBeenCalledWith( - expect.objectContaining({ title: 'Meeting 1' }) - ); - }); - - it('should display day names header', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText('Sun')).toBeInTheDocument(); - expect(screen.getByText('Mon')).toBeInTheDocument(); - expect(screen.getByText('Tue')).toBeInTheDocument(); - expect(screen.getByText('Wed')).toBeInTheDocument(); - expect(screen.getByText('Thu')).toBeInTheDocument(); - expect(screen.getByText('Fri')).toBeInTheDocument(); - expect(screen.getByText('Sat')).toBeInTheDocument(); - }); - }); -}); diff --git a/packages/views/src/__tests__/ObjectGantt.test.tsx b/packages/views/src/__tests__/ObjectGantt.test.tsx deleted file mode 100644 index 7b166937..00000000 --- a/packages/views/src/__tests__/ObjectGantt.test.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/react'; -import { ObjectGantt } from '../ObjectGantt'; -import type { ObjectGridSchema, DataSource } from '@object-ui/types'; - -describe('ObjectGantt', () => { - let mockDataSource: DataSource; - let mockSchema: ObjectGridSchema; - - beforeEach(() => { - // Mock data source - mockDataSource = { - find: vi.fn().mockResolvedValue({ - data: [ - { - _id: '1', - title: 'Task 1', - start_date: '2024-01-01T00:00:00Z', - end_date: '2024-01-15T00:00:00Z', - progress: 50, - }, - { - _id: '2', - title: 'Task 2', - start_date: '2024-01-10T00:00:00Z', - end_date: '2024-01-25T00:00:00Z', - progress: 75, - }, - ], - }), - getObjectSchema: vi.fn().mockResolvedValue({ - name: 'tasks', - label: 'Tasks', - fields: { - title: { type: 'text', label: 'Title' }, - start_date: { type: 'date', label: 'Start Date' }, - end_date: { type: 'date', label: 'End Date' }, - progress: { type: 'number', label: 'Progress' }, - }, - }), - } as any; - - // Mock schema with gantt config - mockSchema = { - type: 'object-grid', - objectName: 'tasks', - data: { - provider: 'object', - object: 'tasks', - }, - filter: { - gantt: { - startDateField: 'start_date', - endDateField: 'end_date', - titleField: 'title', - progressField: 'progress', - }, - }, - } as any; - }); - - it('should render loading state initially', () => { - render(); - expect(screen.getByText(/Loading Gantt chart/i)).toBeInTheDocument(); - }); - - it('should fetch and display gantt data', async () => { - render(); - - await waitFor(() => { - expect(mockDataSource.find).toHaveBeenCalledWith('tasks', expect.any(Object)); - }); - - await waitFor(() => { - expect(screen.getAllByText('Task 1').length).toBeGreaterThan(0); - expect(screen.getAllByText('Task 2').length).toBeGreaterThan(0); - }); - }); - - it('should display task progress', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText('50%')).toBeInTheDocument(); - expect(screen.getByText('75%')).toBeInTheDocument(); - }); - }); - - it('should render with inline data', async () => { - const schemaWithInlineData = { - ...mockSchema, - data: { - provider: 'value', - items: [ - { - id: '1', - title: 'Project A', - start_date: '2024-01-01', - end_date: '2024-01-31', - progress: 30, - }, - ], - }, - } as any; - - render(); - - await waitFor(() => { - expect(screen.getAllByText('Project A').length).toBeGreaterThan(0); - expect(screen.getByText('30%')).toBeInTheDocument(); - }); - }); - - it('should show error when gantt config is missing', async () => { - const schemaWithoutGantt = { - ...mockSchema, - filter: {}, - } as any; - - render(); - - await waitFor(() => { - expect(screen.getByText(/Gantt configuration required/i)).toBeInTheDocument(); - }); - }); - - it('should handle task click', async () => { - const onTaskClick = vi.fn(); - render( - - ); - - await waitFor(() => { - expect(screen.getAllByText('Task 1').length).toBeGreaterThan(0); - }); - - // Click on task in the list - const task = screen.getAllByText('Task 1')[0].closest('div'); - if (task) { - task.click(); - expect(onTaskClick).toHaveBeenCalledWith( - expect.objectContaining({ title: 'Task 1' }) - ); - } - }); - - it('should display task list section', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText('Tasks')).toBeInTheDocument(); - }); - }); - - it('should display date ranges for tasks', async () => { - render(); - - await waitFor(() => { - // Should display formatted dates - const dateElements = screen.getAllByText(/-/); - expect(dateElements.length).toBeGreaterThan(0); - }); - }); -}); diff --git a/packages/views/src/__tests__/ObjectKanban.test.tsx b/packages/views/src/__tests__/ObjectKanban.test.tsx deleted file mode 100644 index 1264a4fc..00000000 --- a/packages/views/src/__tests__/ObjectKanban.test.tsx +++ /dev/null @@ -1,141 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/react'; -import { ObjectKanban } from '../ObjectKanban'; -import type { ObjectGridSchema, DataSource } from '@object-ui/types'; - -describe('ObjectKanban', () => { - let mockDataSource: DataSource; - let mockSchema: ObjectGridSchema; - - beforeEach(() => { - // Mock data source - mockDataSource = { - find: vi.fn().mockResolvedValue({ - data: [ - { _id: '1', title: 'Task 1', status: 'todo', priority: 'high' }, - { _id: '2', title: 'Task 2', status: 'in-progress', priority: 'medium' }, - { _id: '3', title: 'Task 3', status: 'done', priority: 'low' }, - ], - }), - getObjectSchema: vi.fn().mockResolvedValue({ - name: 'tasks', - label: 'Tasks', - fields: { - title: { type: 'text', label: 'Title' }, - status: { type: 'select', label: 'Status' }, - priority: { type: 'select', label: 'Priority' }, - }, - }), - update: vi.fn().mockResolvedValue({ success: true }), - } as any; - - // Mock schema with kanban config - mockSchema = { - type: 'object-grid', - objectName: 'tasks', - data: { - provider: 'object', - object: 'tasks', - }, - filter: { - kanban: { - groupByField: 'status', - columns: ['title', 'priority'], - }, - }, - } as any; - }); - - it('should render loading state initially', () => { - render(); - expect(screen.getByText(/Loading kanban board/i)).toBeInTheDocument(); - }); - - it('should fetch and display kanban data', async () => { - render(); - - await waitFor(() => { - expect(mockDataSource.find).toHaveBeenCalledWith('tasks', expect.any(Object)); - }); - - await waitFor(() => { - expect(screen.getByText('Task 1')).toBeInTheDocument(); - expect(screen.getByText('Task 2')).toBeInTheDocument(); - expect(screen.getByText('Task 3')).toBeInTheDocument(); - }); - }); - - it('should group tasks by status field', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText('todo')).toBeInTheDocument(); - expect(screen.getByText('in-progress')).toBeInTheDocument(); - expect(screen.getByText('done')).toBeInTheDocument(); - }); - }); - - it('should render with inline data', async () => { - const schemaWithInlineData = { - ...mockSchema, - data: { - provider: 'value', - items: [ - { id: '1', title: 'Task A', status: 'todo' }, - { id: '2', title: 'Task B', status: 'done' }, - ], - }, - } as any; - - render(); - - await waitFor(() => { - expect(screen.getByText('Task A')).toBeInTheDocument(); - expect(screen.getByText('Task B')).toBeInTheDocument(); - }); - }); - - it('should show error when kanban config is missing', async () => { - const schemaWithoutKanban = { - ...mockSchema, - filter: {}, - } as any; - - render(); - - await waitFor(() => { - expect(screen.getByText(/Kanban configuration required/i)).toBeInTheDocument(); - }); - }); - - it('should handle card click events', async () => { - const onCardClick = vi.fn(); - render( - - ); - - await waitFor(() => { - expect(screen.getByText('Task 1')).toBeInTheDocument(); - }); - - const card = screen.getByText('Task 1').closest('div'); - if (card) { - card.click(); - expect(onCardClick).toHaveBeenCalledWith( - expect.objectContaining({ title: 'Task 1' }) - ); - } - }); -}); diff --git a/packages/views/src/__tests__/ObjectMap.test.tsx b/packages/views/src/__tests__/ObjectMap.test.tsx deleted file mode 100644 index ea6268a4..00000000 --- a/packages/views/src/__tests__/ObjectMap.test.tsx +++ /dev/null @@ -1,233 +0,0 @@ -/** - * ObjectUI - * Copyright (c) 2024-present ObjectStack Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/react'; -import { ObjectMap } from '../ObjectMap'; -import type { ObjectGridSchema, DataSource } from '@object-ui/types'; - -describe('ObjectMap', () => { - let mockDataSource: DataSource; - let mockSchema: ObjectGridSchema; - - beforeEach(() => { - // Mock data source - mockDataSource = { - find: vi.fn().mockResolvedValue({ - data: [ - { - _id: '1', - name: 'Location 1', - latitude: 40.7128, - longitude: -74.006, - description: 'New York', - }, - { - _id: '2', - name: 'Location 2', - latitude: 34.0522, - longitude: -118.2437, - description: 'Los Angeles', - }, - ], - }), - getObjectSchema: vi.fn().mockResolvedValue({ - name: 'locations', - label: 'Locations', - fields: { - name: { type: 'text', label: 'Name' }, - latitude: { type: 'number', label: 'Latitude' }, - longitude: { type: 'number', label: 'Longitude' }, - description: { type: 'text', label: 'Description' }, - }, - }), - } as any; - - // Mock schema with map config - mockSchema = { - type: 'object-grid', - objectName: 'locations', - data: { - provider: 'object', - object: 'locations', - }, - filter: { - map: { - latitudeField: 'latitude', - longitudeField: 'longitude', - titleField: 'name', - descriptionField: 'description', - }, - }, - } as any; - }); - - it('should render loading state initially', () => { - render(); - expect(screen.getByText(/Loading map/i)).toBeInTheDocument(); - }); - - it('should fetch and display map data', async () => { - render(); - - await waitFor(() => { - expect(mockDataSource.find).toHaveBeenCalledWith('locations', expect.any(Object)); - }); - - await waitFor(() => { - expect(screen.getByText('Location 1')).toBeInTheDocument(); - expect(screen.getByText('Location 2')).toBeInTheDocument(); - }); - }); - - it('should display marker count', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText(/Locations \(2\)/i)).toBeInTheDocument(); - }); - }); - - it('should render with inline data', async () => { - const schemaWithInlineData = { - ...mockSchema, - data: { - provider: 'value', - items: [ - { - id: '1', - name: 'Place A', - latitude: 51.5074, - longitude: -0.1278, - }, - ], - }, - } as any; - - render(); - - await waitFor(() => { - expect(screen.getByText('Place A')).toBeInTheDocument(); - expect(screen.getByText(/Locations \(1\)/i)).toBeInTheDocument(); - }); - }); - - it('should handle location object format', async () => { - const schemaWithLocationField = { - ...mockSchema, - data: { - provider: 'value', - items: [ - { - id: '1', - name: 'Place B', - location: { lat: 48.8566, lng: 2.3522 }, - }, - ], - }, - filter: { - map: { - locationField: 'location', - titleField: 'name', - }, - }, - } as any; - - render(); - - await waitFor(() => { - expect(screen.getByText('Place B')).toBeInTheDocument(); - }); - }); - - it('should handle string location format', async () => { - const schemaWithStringLocation = { - ...mockSchema, - data: { - provider: 'value', - items: [ - { - id: '1', - name: 'Place C', - location: '35.6762, 139.6503', - }, - ], - }, - filter: { - map: { - locationField: 'location', - titleField: 'name', - }, - }, - } as any; - - render(); - - await waitFor(() => { - expect(screen.getByText('Place C')).toBeInTheDocument(); - }); - }); - - it('should display map placeholder', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText(/Map Visualization \(Placeholder\)/i)).toBeInTheDocument(); - }); - }); - - it('should handle marker click', async () => { - const onMarkerClick = vi.fn(); - render( - - ); - - await waitFor(() => { - expect(screen.getByText('Location 1')).toBeInTheDocument(); - }); - - const marker = screen.getByText('Location 1').closest('div'); - if (marker) { - marker.click(); - expect(onMarkerClick).toHaveBeenCalledWith( - expect.objectContaining({ name: 'Location 1' }) - ); - } - }); - - it('should show empty state when no valid coordinates', async () => { - const schemaWithInvalidData = { - ...mockSchema, - data: { - provider: 'value', - items: [ - { id: '1', name: 'Invalid Location' }, - ], - }, - } as any; - - render(); - - await waitFor(() => { - expect(screen.getByText(/No locations found with valid coordinates/i)).toBeInTheDocument(); - }); - }); - - it('should display legend', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText('Legend')).toBeInTheDocument(); - expect(screen.getByText('Location Marker')).toBeInTheDocument(); - }); - }); -}); diff --git a/packages/views/src/index.tsx b/packages/views/src/index.tsx index 3f51a3b5..d8c8d42d 100644 --- a/packages/views/src/index.tsx +++ b/packages/views/src/index.tsx @@ -28,18 +28,6 @@ export type { ObjectFormProps } from './ObjectForm'; export { ObjectView } from './ObjectView'; export type { ObjectViewProps } from './ObjectView'; -export { ObjectKanban } from './ObjectKanban'; -export type { ObjectKanbanProps } from './ObjectKanban'; - -export { ObjectCalendar } from './ObjectCalendar'; -export type { ObjectCalendarProps } from './ObjectCalendar'; - -export { ObjectGantt } from './ObjectGantt'; -export type { ObjectGanttProps } from './ObjectGantt'; - -export { ObjectMap } from './ObjectMap'; -export type { ObjectMapProps } from './ObjectMap'; - // Export field renderers for customization export { getCellRenderer, @@ -74,10 +62,6 @@ export type { import { ObjectGrid } from './ObjectGrid'; import { ObjectForm } from './ObjectForm'; import { ObjectView } from './ObjectView'; -import { ObjectKanban } from './ObjectKanban'; -import { ObjectCalendar } from './ObjectCalendar'; -import { ObjectGantt } from './ObjectGantt'; -import { ObjectMap } from './ObjectMap'; // Create renderer wrappers for ComponentRegistry const ObjectGridRenderer: React.FC<{ schema: any }> = ({ schema }) => { @@ -135,22 +119,6 @@ const ObjectViewRenderer: React.FC<{ schema: any }> = ({ schema }) => { return ; }; -const ObjectKanbanRenderer: React.FC<{ schema: any }> = ({ schema }) => { - return ; -}; - -const ObjectCalendarRenderer: React.FC<{ schema: any }> = ({ schema }) => { - return ; -}; - -const ObjectGanttRenderer: React.FC<{ schema: any }> = ({ schema }) => { - return ; -}; - -const ObjectMapRenderer: React.FC<{ schema: any }> = ({ schema }) => { - return ; -}; - // Register components with ComponentRegistry ComponentRegistry.register('object-grid', ObjectGridRenderer, { label: 'Object Grid', @@ -187,49 +155,9 @@ ComponentRegistry.register('object-view', ObjectViewRenderer, { ], }); -ComponentRegistry.register('object-kanban', ObjectKanbanRenderer, { - label: 'Object Kanban', - category: 'plugin', - inputs: [ - { name: 'objectName', type: 'string', label: 'Object Name', required: true }, - { name: 'kanban', type: 'object', label: 'Kanban Config', description: 'groupByField, columns, summarizeField' }, - ], -}); - -ComponentRegistry.register('object-calendar', ObjectCalendarRenderer, { - label: 'Object Calendar', - category: 'plugin', - inputs: [ - { name: 'objectName', type: 'string', label: 'Object Name', required: true }, - { name: 'calendar', type: 'object', label: 'Calendar Config', description: 'startDateField, endDateField, titleField, colorField' }, - ], -}); - -ComponentRegistry.register('object-gantt', ObjectGanttRenderer, { - label: 'Object Gantt', - category: 'plugin', - inputs: [ - { name: 'objectName', type: 'string', label: 'Object Name', required: true }, - { name: 'gantt', type: 'object', label: 'Gantt Config', description: 'startDateField, endDateField, titleField, progressField, dependenciesField' }, - ], -}); - -ComponentRegistry.register('object-map', ObjectMapRenderer, { - label: 'Object Map', - category: 'plugin', - inputs: [ - { name: 'objectName', type: 'string', label: 'Object Name', required: true }, - { name: 'map', type: 'object', label: 'Map Config', description: 'latitudeField, longitudeField, titleField' }, - ], -}); - // Export for manual use export const objectComponents = { 'object-grid': ObjectGridRenderer, 'object-form': ObjectFormRenderer, 'object-view': ObjectViewRenderer, - 'object-kanban': ObjectKanbanRenderer, - 'object-calendar': ObjectCalendarRenderer, - 'object-gantt': ObjectGanttRenderer, - 'object-map': ObjectMapRenderer, }; From 2214df222c907808a5c5f4bc6dabbf0fa939a473 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:43:19 +0000 Subject: [PATCH 5/5] Consolidate imports in field-renderers for better readability Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/views/src/field-renderers.tsx | 4 +- pnpm-lock.yaml | 129 +++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/packages/views/src/field-renderers.tsx b/packages/views/src/field-renderers.tsx index 08016207..314d46c4 100644 --- a/packages/views/src/field-renderers.tsx +++ b/packages/views/src/field-renderers.tsx @@ -15,9 +15,7 @@ import React from 'react'; import type { FieldMetadata, SelectOptionMetadata } from '@object-ui/types'; -import { Badge } from '@object-ui/components'; -import { Avatar, AvatarFallback } from '@object-ui/components'; -import { Button } from '@object-ui/components'; +import { Badge, Avatar, AvatarFallback, Button } from '@object-ui/components'; import { Check, X } from 'lucide-react'; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83a092e3..8e4219d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -489,6 +489,49 @@ importers: specifier: ^4.5.4 version: 4.5.4(@types/node@25.0.10)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)) + packages/plugin-calendar: + dependencies: + '@object-ui/components': + specifier: workspace:* + version: link:../components + '@object-ui/core': + specifier: workspace:* + version: link:../core + '@object-ui/react': + specifier: workspace:* + version: link:../react + '@object-ui/types': + specifier: workspace:* + version: link:../types + lucide-react: + specifier: ^0.563.0 + version: 0.563.0(react@19.2.3) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + devDependencies: + '@types/react': + specifier: 19.2.9 + version: 19.2.9 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.9) + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.7.0(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2) + vite-plugin-dts: + specifier: ^4.5.4 + version: 4.5.4(@types/node@25.0.10)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)) + packages/plugin-calendar-view: dependencies: '@object-ui/components': @@ -661,6 +704,49 @@ importers: specifier: ^4.5.4 version: 4.5.4(@types/node@25.0.10)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)) + packages/plugin-gantt: + dependencies: + '@object-ui/components': + specifier: workspace:* + version: link:../components + '@object-ui/core': + specifier: workspace:* + version: link:../core + '@object-ui/react': + specifier: workspace:* + version: link:../react + '@object-ui/types': + specifier: workspace:* + version: link:../types + lucide-react: + specifier: ^0.563.0 + version: 0.563.0(react@19.2.3) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + devDependencies: + '@types/react': + specifier: 19.2.9 + version: 19.2.9 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.9) + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.7.0(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2) + vite-plugin-dts: + specifier: ^4.5.4 + version: 4.5.4(@types/node@25.0.10)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)) + packages/plugin-kanban: dependencies: '@dnd-kit/core': @@ -710,6 +796,49 @@ importers: specifier: ^4.5.4 version: 4.5.4(@types/node@25.0.10)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)) + packages/plugin-map: + dependencies: + '@object-ui/components': + specifier: workspace:* + version: link:../components + '@object-ui/core': + specifier: workspace:* + version: link:../core + '@object-ui/react': + specifier: workspace:* + version: link:../react + '@object-ui/types': + specifier: workspace:* + version: link:../types + lucide-react: + specifier: ^0.563.0 + version: 0.563.0(react@19.2.3) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + devDependencies: + '@types/react': + specifier: 19.2.9 + version: 19.2.9 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.9) + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.7.0(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2) + vite-plugin-dts: + specifier: ^4.5.4 + version: 4.5.4(@types/node@25.0.10)(rollup@4.55.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)) + packages/plugin-markdown: dependencies: '@object-ui/components':