Skip to content

Commit f1c83aa

Browse files
authored
Merge pull request #391 from objectstack-ai/copilot/update-object-agrid-rendering
2 parents ec5d799 + 97ae174 commit f1c83aa

5 files changed

Lines changed: 678 additions & 169 deletions

File tree

packages/plugin-aggrid/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"dependencies": {
3535
"@object-ui/components": "workspace:*",
3636
"@object-ui/core": "workspace:*",
37+
"@object-ui/fields": "workspace:*",
3738
"@object-ui/react": "workspace:*",
3839
"@object-ui/types": "workspace:*",
3940
"@object-ui/data-objectstack": "workspace:*"

packages/plugin-aggrid/src/ObjectAgGridImpl.tsx

Lines changed: 67 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,11 @@ import type {
1818
StatusPanelDef,
1919
GetContextMenuItemsParams,
2020
MenuItemDef,
21-
IServerSideDatasource,
22-
IServerSideGetRowsParams
2321
} from 'ag-grid-community';
24-
import type { DataSource, FieldMetadata, ObjectSchemaMetadata } from '@object-ui/types';
22+
import type { FieldMetadata, ObjectSchemaMetadata } from '@object-ui/types';
2523
import type { ObjectAgGridImplProps } from './object-aggrid.types';
2624
import { FIELD_TYPE_TO_FILTER_TYPE } from './object-aggrid.types';
25+
import { createFieldCellRenderer, createFieldCellEditor } from './field-renderers';
2726

2827
/**
2928
* ObjectAgGridImpl - Metadata-driven AG Grid implementation
@@ -61,7 +60,6 @@ export default function ObjectAgGridImpl({
6160
const [error, setError] = useState<Error | null>(null);
6261
const [objectSchema, setObjectSchema] = useState<ObjectSchemaMetadata | null>(null);
6362
const [rowData, setRowData] = useState<any[]>([]);
64-
const [totalCount, setTotalCount] = useState(0);
6563

6664
// Fetch object metadata
6765
useEffect(() => {
@@ -115,7 +113,6 @@ export default function ObjectAgGridImpl({
115113

116114
const result = await dataSource.find(objectName, queryParams);
117115
setRowData(result.data || []);
118-
setTotalCount(result.total || 0);
119116
callbacks?.onDataLoaded?.(result.data || []);
120117
} catch (err) {
121118
const error = err instanceof Error ? err : new Error(String(err));
@@ -416,15 +413,6 @@ export default function ObjectAgGridImpl({
416413
);
417414
}
418415

419-
/**
420-
* Escape HTML to prevent XSS attacks
421-
*/
422-
function escapeHtml(text: string): string {
423-
const div = document.createElement('div');
424-
div.textContent = text;
425-
return div.innerHTML;
426-
}
427-
428416
/**
429417
* Get filter type based on field metadata
430418
*/
@@ -438,166 +426,76 @@ function getFilterType(field: FieldMetadata): string | boolean {
438426

439427
/**
440428
* Apply field type-specific formatting to column definition
429+
* Uses field widgets from @object-ui/fields for consistent rendering
441430
*/
442431
function applyFieldTypeFormatting(colDef: ColDef, field: FieldMetadata): void {
443-
switch (field.type) {
444-
case 'boolean':
445-
colDef.cellRenderer = (params: any) => {
446-
if (params.value === true) return '✓ Yes';
447-
if (params.value === false) return '✗ No';
448-
return '';
449-
};
450-
break;
451-
452-
case 'currency':
453-
colDef.valueFormatter = (params: any) => {
454-
if (params.value == null) return '';
455-
const currency = (field as any).currency || 'USD';
456-
const precision = (field as any).precision || 2;
457-
return new Intl.NumberFormat('en-US', {
458-
style: 'currency',
459-
currency,
460-
minimumFractionDigits: precision,
461-
maximumFractionDigits: precision,
462-
}).format(params.value);
463-
};
464-
break;
465-
466-
case 'percent':
467-
colDef.valueFormatter = (params: any) => {
468-
if (params.value == null) return '';
469-
const precision = (field as any).precision || 2;
470-
return `${(params.value * 100).toFixed(precision)}%`;
471-
};
472-
break;
473-
474-
case 'date':
475-
colDef.valueFormatter = (params: any) => {
476-
if (!params.value) return '';
477-
try {
478-
const date = new Date(params.value);
479-
if (isNaN(date.getTime())) return '';
480-
return date.toLocaleDateString();
481-
} catch {
482-
return '';
483-
}
484-
};
485-
break;
486-
487-
case 'datetime':
488-
colDef.valueFormatter = (params: any) => {
489-
if (!params.value) return '';
490-
try {
491-
const date = new Date(params.value);
492-
if (isNaN(date.getTime())) return '';
493-
return date.toLocaleString();
494-
} catch {
495-
return '';
496-
}
497-
};
498-
break;
499-
500-
case 'time':
501-
colDef.valueFormatter = (params: any) => {
502-
if (!params.value) return '';
503-
return params.value;
504-
};
505-
break;
506-
507-
case 'email':
508-
colDef.cellRenderer = (params: any) => {
509-
if (!params.value) return '';
510-
const escaped = escapeHtml(params.value);
511-
return `<a href="mailto:${escaped}" class="text-blue-600 hover:underline">${escaped}</a>`;
512-
};
513-
break;
514-
515-
case 'url':
516-
colDef.cellRenderer = (params: any) => {
517-
if (!params.value) return '';
518-
const escaped = escapeHtml(params.value);
519-
return `<a href="${escaped}" target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:underline">${escaped}</a>`;
520-
};
521-
break;
522-
523-
case 'phone':
524-
colDef.cellRenderer = (params: any) => {
525-
if (!params.value) return '';
526-
const escaped = escapeHtml(params.value);
527-
return `<a href="tel:${escaped}" class="text-blue-600 hover:underline">${escaped}</a>`;
528-
};
529-
break;
530-
531-
case 'select':
532-
colDef.valueFormatter = (params: any) => {
533-
if (!params.value) return '';
534-
const options = (field as any).options || [];
535-
const option = options.find((opt: any) => opt.value === params.value);
536-
return option?.label || params.value;
537-
};
538-
break;
539-
540-
case 'lookup':
541-
case 'master_detail':
542-
colDef.valueFormatter = (params: any) => {
543-
if (!params.value) return '';
544-
// Handle lookup values - could be an object or just an ID
545-
if (typeof params.value === 'object') {
546-
return params.value.name || params.value.label || params.value.id || '';
547-
}
548-
return String(params.value);
549-
};
550-
break;
432+
// Define field types that should use field widgets for rendering
433+
const fieldWidgetTypes = [
434+
'text', 'textarea', 'number', 'currency', 'percent',
435+
'boolean', 'select', 'date', 'datetime', 'time',
436+
'email', 'phone', 'url', 'password', 'color',
437+
'rating', 'image', 'avatar', 'lookup', 'slider', 'code'
438+
];
439+
440+
// Use field widget renderer if the type is supported
441+
if (fieldWidgetTypes.includes(field.type)) {
442+
colDef.cellRenderer = createFieldCellRenderer(field);
443+
444+
// Add cell editor for editable fields
445+
if (colDef.editable) {
446+
colDef.cellEditor = createFieldCellEditor(field);
551447

552-
case 'number': {
553-
const precision = (field as any).precision;
554-
if (precision !== undefined) {
448+
// Configure editor based on field type
449+
if (['date', 'datetime', 'select', 'lookup', 'color'].includes(field.type)) {
450+
colDef.cellEditorPopup = true;
451+
}
452+
}
453+
} else {
454+
// Fallback to simple rendering for unsupported types
455+
switch (field.type) {
456+
case 'master_detail':
555457
colDef.valueFormatter = (params: any) => {
556-
if (params.value == null) return '';
557-
return Number(params.value).toFixed(precision);
458+
if (!params.value) return '';
459+
// Handle lookup values - could be an object or just an ID
460+
if (typeof params.value === 'object') {
461+
return params.value.name || params.value.label || params.value.id || '';
462+
}
463+
return String(params.value);
464+
};
465+
break;
466+
467+
case 'object':
468+
colDef.cellRenderer = () => {
469+
const span = document.createElement('span');
470+
span.className = 'text-gray-500 italic';
471+
span.textContent = '[Object]';
472+
return span;
473+
};
474+
break;
475+
476+
case 'vector':
477+
colDef.cellRenderer = () => {
478+
const span = document.createElement('span');
479+
span.className = 'text-gray-500 italic';
480+
span.textContent = '[Vector]';
481+
return span;
482+
};
483+
break;
484+
485+
case 'grid':
486+
colDef.cellRenderer = () => {
487+
const span = document.createElement('span');
488+
span.className = 'text-gray-500 italic';
489+
span.textContent = '[Grid]';
490+
return span;
491+
};
492+
break;
493+
494+
default:
495+
// Default text rendering
496+
colDef.valueFormatter = (params: any) => {
497+
return params.value != null ? String(params.value) : '';
558498
};
559-
}
560-
break;
561499
}
562-
563-
case 'color':
564-
colDef.cellRenderer = (params: any) => {
565-
if (!params.value) return '';
566-
const escaped = escapeHtml(params.value);
567-
return `<div class="flex items-center gap-2">
568-
<div style="width: 16px; height: 16px; background-color: ${escaped}; border: 1px solid #ccc; border-radius: 2px;"></div>
569-
<span>${escaped}</span>
570-
</div>`;
571-
};
572-
break;
573-
574-
case 'rating':
575-
colDef.cellRenderer = (params: any) => {
576-
if (params.value == null) return '';
577-
const max = (field as any).max || 5;
578-
const stars = '⭐'.repeat(Math.min(params.value, max));
579-
return stars;
580-
};
581-
break;
582-
583-
case 'image':
584-
colDef.cellRenderer = (params: any) => {
585-
if (!params.value) return '';
586-
const url = typeof params.value === 'string' ? params.value : params.value.url;
587-
if (!url) return '';
588-
const escapedUrl = escapeHtml(url);
589-
return `<img src="${escapedUrl}" alt="" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;" />`;
590-
};
591-
break;
592-
593-
case 'avatar':
594-
colDef.cellRenderer = (params: any) => {
595-
if (!params.value) return '';
596-
const url = typeof params.value === 'string' ? params.value : params.value.url;
597-
if (!url) return '';
598-
const escapedUrl = escapeHtml(url);
599-
return `<img src="${escapedUrl}" alt="" style="width: 32px; height: 32px; object-fit: cover; border-radius: 50%;" />`;
600-
};
601-
break;
602500
}
603501
}

0 commit comments

Comments
 (0)