From a52d526310c3d23ec8e0bb78343b4ca7061b7544 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Feb 2026 04:43:13 +0000
Subject: [PATCH 1/5] Initial plan
From b35e5297b95b35c79f569a3172b75fc0fb4a1685 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Feb 2026 04:48:09 +0000
Subject: [PATCH 2/5] feat: Integrate @object-ui/fields widgets into
object-agrid cell rendering
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
---
packages/plugin-aggrid/package.json | 1 +
.../plugin-aggrid/src/ObjectAgGridImpl.tsx | 207 +++++------------
.../plugin-aggrid/src/field-renderers.tsx | 217 ++++++++++++++++++
pnpm-lock.yaml | 56 +++++
4 files changed, 326 insertions(+), 155 deletions(-)
create mode 100644 packages/plugin-aggrid/src/field-renderers.tsx
diff --git a/packages/plugin-aggrid/package.json b/packages/plugin-aggrid/package.json
index db0c9ede..3a20f4a2 100644
--- a/packages/plugin-aggrid/package.json
+++ b/packages/plugin-aggrid/package.json
@@ -34,6 +34,7 @@
"dependencies": {
"@object-ui/components": "workspace:*",
"@object-ui/core": "workspace:*",
+ "@object-ui/fields": "workspace:*",
"@object-ui/react": "workspace:*",
"@object-ui/types": "workspace:*",
"@object-ui/data-objectstack": "workspace:*"
diff --git a/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx b/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx
index 5b33d42a..96861518 100644
--- a/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx
+++ b/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx
@@ -24,6 +24,7 @@ import type {
import type { DataSource, FieldMetadata, ObjectSchemaMetadata } from '@object-ui/types';
import type { ObjectAgGridImplProps } from './object-aggrid.types';
import { FIELD_TYPE_TO_FILTER_TYPE } from './object-aggrid.types';
+import { createFieldCellRenderer, createFieldCellEditor } from './field-renderers';
/**
* ObjectAgGridImpl - Metadata-driven AG Grid implementation
@@ -438,166 +439,62 @@ function getFilterType(field: FieldMetadata): string | boolean {
/**
* Apply field type-specific formatting to column definition
+ * Uses field widgets from @object-ui/fields for consistent rendering
*/
function applyFieldTypeFormatting(colDef: ColDef, field: FieldMetadata): void {
- switch (field.type) {
- case 'boolean':
- colDef.cellRenderer = (params: any) => {
- if (params.value === true) return '✓ Yes';
- if (params.value === false) return '✗ No';
- return '';
- };
- break;
-
- case 'currency':
- colDef.valueFormatter = (params: any) => {
- if (params.value == null) return '';
- const currency = (field as any).currency || 'USD';
- const precision = (field as any).precision || 2;
- return new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency,
- minimumFractionDigits: precision,
- maximumFractionDigits: precision,
- }).format(params.value);
- };
- break;
-
- case 'percent':
- colDef.valueFormatter = (params: any) => {
- if (params.value == null) return '';
- const precision = (field as any).precision || 2;
- return `${(params.value * 100).toFixed(precision)}%`;
- };
- break;
-
- case 'date':
- colDef.valueFormatter = (params: any) => {
- if (!params.value) return '';
- try {
- const date = new Date(params.value);
- if (isNaN(date.getTime())) return '';
- return date.toLocaleDateString();
- } catch {
- return '';
- }
- };
- break;
-
- case 'datetime':
- colDef.valueFormatter = (params: any) => {
- if (!params.value) return '';
- try {
- const date = new Date(params.value);
- if (isNaN(date.getTime())) return '';
- return date.toLocaleString();
- } catch {
- return '';
- }
- };
- break;
-
- case 'time':
- colDef.valueFormatter = (params: any) => {
- if (!params.value) return '';
- return params.value;
- };
- break;
-
- case 'email':
- colDef.cellRenderer = (params: any) => {
- if (!params.value) return '';
- const escaped = escapeHtml(params.value);
- return `${escaped}`;
- };
- break;
-
- case 'url':
- colDef.cellRenderer = (params: any) => {
- if (!params.value) return '';
- const escaped = escapeHtml(params.value);
- return `${escaped}`;
- };
- break;
-
- case 'phone':
- colDef.cellRenderer = (params: any) => {
- if (!params.value) return '';
- const escaped = escapeHtml(params.value);
- return `${escaped}`;
- };
- break;
-
- case 'select':
- colDef.valueFormatter = (params: any) => {
- if (!params.value) return '';
- const options = (field as any).options || [];
- const option = options.find((opt: any) => opt.value === params.value);
- return option?.label || params.value;
- };
- break;
-
- case 'lookup':
- case 'master_detail':
- colDef.valueFormatter = (params: any) => {
- if (!params.value) return '';
- // Handle lookup values - could be an object or just an ID
- if (typeof params.value === 'object') {
- return params.value.name || params.value.label || params.value.id || '';
- }
- return String(params.value);
- };
- break;
+ // Define field types that should use field widgets for rendering
+ const fieldWidgetTypes = [
+ 'text', 'textarea', 'number', 'currency', 'percent',
+ 'boolean', 'select', 'date', 'datetime', 'time',
+ 'email', 'phone', 'url', 'password', 'color',
+ 'rating', 'image', 'avatar', 'lookup', 'slider', 'code'
+ ];
+
+ // Use field widget renderer if the type is supported
+ if (fieldWidgetTypes.includes(field.type)) {
+ colDef.cellRenderer = createFieldCellRenderer(field);
+
+ // Add cell editor for editable fields
+ if (colDef.editable) {
+ colDef.cellEditor = createFieldCellEditor(field);
- case 'number': {
- const precision = (field as any).precision;
- if (precision !== undefined) {
+ // Configure editor based on field type
+ if (['date', 'datetime', 'select', 'lookup', 'color'].includes(field.type)) {
+ colDef.cellEditorPopup = true;
+ }
+ }
+ } else {
+ // Fallback to simple rendering for unsupported types
+ switch (field.type) {
+ case 'lookup':
+ case 'master_detail':
colDef.valueFormatter = (params: any) => {
- if (params.value == null) return '';
- return Number(params.value).toFixed(precision);
+ if (!params.value) return '';
+ // Handle lookup values - could be an object or just an ID
+ if (typeof params.value === 'object') {
+ return params.value.name || params.value.label || params.value.id || '';
+ }
+ return String(params.value);
+ };
+ break;
+
+ case 'object':
+ colDef.cellRenderer = () => '[Object]';
+ break;
+
+ case 'vector':
+ colDef.cellRenderer = () => '[Vector]';
+ break;
+
+ case 'grid':
+ colDef.cellRenderer = () => '[Grid]';
+ break;
+
+ default:
+ // Default text rendering
+ colDef.valueFormatter = (params: any) => {
+ return params.value != null ? String(params.value) : '';
};
- }
- break;
}
-
- case 'color':
- colDef.cellRenderer = (params: any) => {
- if (!params.value) return '';
- const escaped = escapeHtml(params.value);
- return `
`;
- };
- break;
-
- case 'rating':
- colDef.cellRenderer = (params: any) => {
- if (params.value == null) return '';
- const max = (field as any).max || 5;
- const stars = '⭐'.repeat(Math.min(params.value, max));
- return stars;
- };
- break;
-
- case 'image':
- colDef.cellRenderer = (params: any) => {
- if (!params.value) return '';
- const url = typeof params.value === 'string' ? params.value : params.value.url;
- if (!url) return '';
- const escapedUrl = escapeHtml(url);
- return `
`;
- };
- break;
-
- case 'avatar':
- colDef.cellRenderer = (params: any) => {
- if (!params.value) return '';
- const url = typeof params.value === 'string' ? params.value : params.value.url;
- if (!url) return '';
- const escapedUrl = escapeHtml(url);
- return `
`;
- };
- break;
}
}
diff --git a/packages/plugin-aggrid/src/field-renderers.tsx b/packages/plugin-aggrid/src/field-renderers.tsx
new file mode 100644
index 00000000..b99bf877
--- /dev/null
+++ b/packages/plugin-aggrid/src/field-renderers.tsx
@@ -0,0 +1,217 @@
+/**
+ * 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 { createRoot } from 'react-dom/client';
+import type { ICellRendererParams, ICellEditorParams } from 'ag-grid-community';
+import type { FieldMetadata } from '@object-ui/types';
+
+// Import field widgets
+import {
+ TextField,
+ NumberField,
+ BooleanField,
+ SelectField,
+ DateField,
+ DateTimeField,
+ TimeField,
+ EmailField,
+ PhoneField,
+ UrlField,
+ CurrencyField,
+ PercentField,
+ PasswordField,
+ TextAreaField,
+ ColorField,
+ RatingField,
+ ImageField,
+ AvatarField,
+ LookupField,
+ SliderField,
+ CodeField,
+} from '@object-ui/fields';
+
+/**
+ * Map field type to field widget component
+ */
+function getFieldWidget(fieldType: string): React.ComponentType | null {
+ const widgetMap: Record> = {
+ text: TextField,
+ textarea: TextAreaField,
+ number: NumberField,
+ currency: CurrencyField,
+ percent: PercentField,
+ boolean: BooleanField,
+ select: SelectField,
+ date: DateField,
+ datetime: DateTimeField,
+ time: TimeField,
+ email: EmailField,
+ phone: PhoneField,
+ url: UrlField,
+ password: PasswordField,
+ color: ColorField,
+ rating: RatingField,
+ image: ImageField,
+ avatar: AvatarField,
+ lookup: LookupField,
+ slider: SliderField,
+ code: CodeField,
+ };
+
+ return widgetMap[fieldType] || null;
+}
+
+/**
+ * AG Grid Cell Renderer using Field Widgets (Read-only mode)
+ */
+export class FieldWidgetCellRenderer {
+ public eGui!: HTMLDivElement;
+ public root: any;
+
+ init(params: ICellRendererParams & { field: FieldMetadata }) {
+ const { value, field } = params;
+ const FieldWidget = getFieldWidget(field.type);
+
+ this.eGui = document.createElement('div');
+ this.eGui.className = 'field-widget-cell';
+
+ if (FieldWidget) {
+ this.root = createRoot(this.eGui);
+ this.root.render(
+ {}} // No-op for read-only mode
+ field={field}
+ readonly={true}
+ />
+ );
+ } else {
+ // Fallback to text display
+ this.eGui.textContent = value != null ? String(value) : '';
+ }
+ }
+
+ getGui() {
+ return this.eGui;
+ }
+
+ refresh(params: ICellRendererParams & { field: FieldMetadata }): boolean {
+ const { value, field } = params;
+ const FieldWidget = getFieldWidget(field.type);
+
+ if (FieldWidget && this.root) {
+ this.root.render(
+ {}} // No-op for read-only mode
+ field={field}
+ readonly={true}
+ />
+ );
+ return true;
+ }
+
+ return false;
+ }
+
+ destroy() {
+ if (this.root) {
+ this.root.unmount();
+ }
+ }
+}
+
+/**
+ * AG Grid Cell Editor using Field Widgets (Edit mode)
+ */
+export class FieldWidgetCellEditor {
+ public eGui!: HTMLDivElement;
+ public root: any;
+ public currentValue: any;
+ public params!: ICellEditorParams & { field: FieldMetadata };
+
+ init(params: ICellEditorParams & { field: FieldMetadata }) {
+ this.params = params;
+ this.currentValue = params.value;
+ const { field } = params;
+ const FieldWidget = getFieldWidget(field.type);
+
+ this.eGui = document.createElement('div');
+ this.eGui.className = 'field-widget-editor';
+
+ if (FieldWidget) {
+ this.root = createRoot(this.eGui);
+ this.root.render(
+ {
+ this.currentValue = newValue;
+ }}
+ field={field}
+ readonly={false}
+ />
+ );
+ } else {
+ // Fallback to input element
+ const input = document.createElement('input');
+ input.value = this.currentValue != null ? String(this.currentValue) : '';
+ input.className = 'ag-input-field-input ag-text-field-input';
+ input.addEventListener('input', (e) => {
+ this.currentValue = (e.target as HTMLInputElement).value;
+ });
+ this.eGui.appendChild(input);
+ setTimeout(() => input.focus(), 0);
+ }
+ }
+
+ getGui() {
+ return this.eGui;
+ }
+
+ getValue() {
+ return this.currentValue;
+ }
+
+ destroy() {
+ if (this.root) {
+ this.root.unmount();
+ }
+ }
+
+ isPopup(): boolean {
+ // Return true for complex widgets that need more space
+ const popupTypes = ['date', 'datetime', 'select', 'lookup', 'color'];
+ return popupTypes.includes(this.params.field.type);
+ }
+}
+
+/**
+ * Factory function to create cell renderer with field metadata
+ */
+export function createFieldCellRenderer(field: FieldMetadata) {
+ return class extends FieldWidgetCellRenderer {
+ init(params: ICellRendererParams) {
+ super.init({ ...params, field });
+ }
+ refresh(params: ICellRendererParams): boolean {
+ return super.refresh({ ...params, field });
+ }
+ };
+}
+
+/**
+ * Factory function to create cell editor with field metadata
+ */
+export function createFieldCellEditor(field: FieldMetadata) {
+ return class extends FieldWidgetCellEditor {
+ init(params: ICellEditorParams) {
+ super.init({ ...params, field });
+ }
+ };
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index df690d7d..c6928ad3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -997,6 +997,9 @@ importers:
'@object-ui/data-objectstack':
specifier: workspace:*
version: link:../data-objectstack
+ '@object-ui/fields':
+ specifier: workspace:*
+ version: link:../fields
'@object-ui/react':
specifier: workspace:*
version: link:../react
@@ -2949,89 +2952,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -3347,24 +3366,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@next/swc-linux-arm64-musl@16.1.6':
resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@next/swc-linux-x64-gnu@16.1.6':
resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@next/swc-linux-x64-musl@16.1.6':
resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@next/swc-win32-arm64-msvc@16.1.6':
resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==}
@@ -4186,66 +4209,79 @@ packages:
resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.57.1':
resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.57.1':
resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.57.1':
resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.57.1':
resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==}
cpu: [loong64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.57.1':
resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==}
cpu: [loong64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.57.1':
resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.57.1':
resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==}
cpu: [ppc64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.57.1':
resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.57.1':
resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.57.1':
resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.57.1':
resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.57.1':
resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-openbsd-x64@4.57.1':
resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==}
@@ -4629,24 +4665,28 @@ packages:
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@swc/core-linux-arm64-musl@1.15.11':
resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@swc/core-linux-x64-gnu@1.15.11':
resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@swc/core-linux-x64-musl@1.15.11':
resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@swc/core-win32-arm64-msvc@1.15.11':
resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==}
@@ -4728,24 +4768,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
@@ -5172,41 +5216,49 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -7840,24 +7892,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
From d88c02dcce263c7af53ecb6915a2ae95b5ce44d8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Feb 2026 04:50:48 +0000
Subject: [PATCH 3/5] chore: Clean up unused imports and variables
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
---
packages/plugin-aggrid/src/ObjectAgGridImpl.tsx | 15 +--------------
1 file changed, 1 insertion(+), 14 deletions(-)
diff --git a/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx b/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx
index 96861518..07d615d9 100644
--- a/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx
+++ b/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx
@@ -18,10 +18,8 @@ import type {
StatusPanelDef,
GetContextMenuItemsParams,
MenuItemDef,
- IServerSideDatasource,
- IServerSideGetRowsParams
} from 'ag-grid-community';
-import type { DataSource, FieldMetadata, ObjectSchemaMetadata } from '@object-ui/types';
+import type { FieldMetadata, ObjectSchemaMetadata } from '@object-ui/types';
import type { ObjectAgGridImplProps } from './object-aggrid.types';
import { FIELD_TYPE_TO_FILTER_TYPE } from './object-aggrid.types';
import { createFieldCellRenderer, createFieldCellEditor } from './field-renderers';
@@ -62,7 +60,6 @@ export default function ObjectAgGridImpl({
const [error, setError] = useState(null);
const [objectSchema, setObjectSchema] = useState(null);
const [rowData, setRowData] = useState([]);
- const [totalCount, setTotalCount] = useState(0);
// Fetch object metadata
useEffect(() => {
@@ -116,7 +113,6 @@ export default function ObjectAgGridImpl({
const result = await dataSource.find(objectName, queryParams);
setRowData(result.data || []);
- setTotalCount(result.total || 0);
callbacks?.onDataLoaded?.(result.data || []);
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
@@ -417,15 +413,6 @@ export default function ObjectAgGridImpl({
);
}
-/**
- * Escape HTML to prevent XSS attacks
- */
-function escapeHtml(text: string): string {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
-}
-
/**
* Get filter type based on field metadata
*/
From 00d17dda42338d0749dadc3f96e71cce6f994fb5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Feb 2026 04:52:49 +0000
Subject: [PATCH 4/5] fix: Address code review feedback - improve type safety
and use DOM elements for fallback renderers
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
---
.../plugin-aggrid/src/ObjectAgGridImpl.tsx | 21 ++++++++++++++++---
.../plugin-aggrid/src/field-renderers.tsx | 8 +++----
2 files changed, 22 insertions(+), 7 deletions(-)
diff --git a/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx b/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx
index 07d615d9..f896eb36 100644
--- a/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx
+++ b/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx
@@ -466,15 +466,30 @@ function applyFieldTypeFormatting(colDef: ColDef, field: FieldMetadata): void {
break;
case 'object':
- colDef.cellRenderer = () => '[Object]';
+ colDef.cellRenderer = () => {
+ const span = document.createElement('span');
+ span.className = 'text-gray-500 italic';
+ span.textContent = '[Object]';
+ return span;
+ };
break;
case 'vector':
- colDef.cellRenderer = () => '[Vector]';
+ colDef.cellRenderer = () => {
+ const span = document.createElement('span');
+ span.className = 'text-gray-500 italic';
+ span.textContent = '[Vector]';
+ return span;
+ };
break;
case 'grid':
- colDef.cellRenderer = () => '[Grid]';
+ colDef.cellRenderer = () => {
+ const span = document.createElement('span');
+ span.className = 'text-gray-500 italic';
+ span.textContent = '[Grid]';
+ return span;
+ };
break;
default:
diff --git a/packages/plugin-aggrid/src/field-renderers.tsx b/packages/plugin-aggrid/src/field-renderers.tsx
index b99bf877..652adb24 100644
--- a/packages/plugin-aggrid/src/field-renderers.tsx
+++ b/packages/plugin-aggrid/src/field-renderers.tsx
@@ -7,7 +7,7 @@
*/
import React from 'react';
-import { createRoot } from 'react-dom/client';
+import { createRoot, type Root } from 'react-dom/client';
import type { ICellRendererParams, ICellEditorParams } from 'ag-grid-community';
import type { FieldMetadata } from '@object-ui/types';
@@ -72,7 +72,7 @@ function getFieldWidget(fieldType: string): React.ComponentType | null {
*/
export class FieldWidgetCellRenderer {
public eGui!: HTMLDivElement;
- public root: any;
+ public root: Root | null = null;
init(params: ICellRendererParams & { field: FieldMetadata }) {
const { value, field } = params;
@@ -132,8 +132,8 @@ export class FieldWidgetCellRenderer {
*/
export class FieldWidgetCellEditor {
public eGui!: HTMLDivElement;
- public root: any;
- public currentValue: any;
+ public root: Root | null = null;
+ public currentValue: unknown;
public params!: ICellEditorParams & { field: FieldMetadata };
init(params: ICellEditorParams & { field: FieldMetadata }) {
From 97ae174c3fca788577704e45a4aa28cec92c3e58 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Feb 2026 05:14:59 +0000
Subject: [PATCH 5/5] fix: Address code review feedback - optimize performance,
improve refresh logic, and add comprehensive test coverage
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
---
.../plugin-aggrid/src/ObjectAgGridImpl.tsx | 1 -
.../src/field-renderers.test.tsx | 383 ++++++++++++++++++
.../plugin-aggrid/src/field-renderers.tsx | 55 +--
3 files changed, 414 insertions(+), 25 deletions(-)
create mode 100644 packages/plugin-aggrid/src/field-renderers.test.tsx
diff --git a/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx b/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx
index f896eb36..66c76c66 100644
--- a/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx
+++ b/packages/plugin-aggrid/src/ObjectAgGridImpl.tsx
@@ -453,7 +453,6 @@ function applyFieldTypeFormatting(colDef: ColDef, field: FieldMetadata): void {
} else {
// Fallback to simple rendering for unsupported types
switch (field.type) {
- case 'lookup':
case 'master_detail':
colDef.valueFormatter = (params: any) => {
if (!params.value) return '';
diff --git a/packages/plugin-aggrid/src/field-renderers.test.tsx b/packages/plugin-aggrid/src/field-renderers.test.tsx
new file mode 100644
index 00000000..952c5b2d
--- /dev/null
+++ b/packages/plugin-aggrid/src/field-renderers.test.tsx
@@ -0,0 +1,383 @@
+/**
+ * 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, afterEach } from 'vitest';
+import type { ICellRendererParams, ICellEditorParams } from 'ag-grid-community';
+import type { FieldMetadata } from '@object-ui/types';
+import {
+ FieldWidgetCellRenderer,
+ FieldWidgetCellEditor,
+ createFieldCellRenderer,
+ createFieldCellEditor
+} from './field-renderers';
+
+describe('field-renderers', () => {
+ describe('FieldWidgetCellRenderer', () => {
+ let renderer: FieldWidgetCellRenderer;
+
+ beforeEach(() => {
+ renderer = new FieldWidgetCellRenderer();
+ });
+
+ afterEach(() => {
+ renderer.destroy();
+ });
+
+ it('should initialize with a field widget for supported types', () => {
+ const field: FieldMetadata = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'text',
+ };
+
+ const params = {
+ value: 'test value',
+ field,
+ } as ICellRendererParams & { field: FieldMetadata };
+
+ renderer.init(params);
+
+ expect(renderer.eGui).toBeDefined();
+ expect(renderer.eGui.className).toBe('field-widget-cell');
+ expect(renderer.root).toBeDefined();
+ });
+
+ it('should initialize with fallback for unsupported types', () => {
+ const field = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'unsupported_type',
+ } as unknown as FieldMetadata;
+
+ const params = {
+ value: 'fallback value',
+ field,
+ } as ICellRendererParams & { field: FieldMetadata };
+
+ renderer.init(params);
+
+ expect(renderer.eGui).toBeDefined();
+ expect(renderer.eGui.textContent).toBe('fallback value');
+ expect(renderer.root).toBeNull();
+ });
+
+ it('should handle null values in fallback mode', () => {
+ const field = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'unsupported_type',
+ } as unknown as FieldMetadata;
+
+ const params = {
+ value: null,
+ field,
+ } as ICellRendererParams & { field: FieldMetadata };
+
+ renderer.init(params);
+
+ expect(renderer.eGui.textContent).toBe('');
+ });
+
+ it('should return the GUI element', () => {
+ const field: FieldMetadata = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'text',
+ };
+
+ const params = {
+ value: 'test',
+ field,
+ } as ICellRendererParams & { field: FieldMetadata };
+
+ renderer.init(params);
+ const gui = renderer.getGui();
+
+ expect(gui).toBe(renderer.eGui);
+ });
+
+ it('should refresh with new value for supported field type', () => {
+ const field: FieldMetadata = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'text',
+ };
+
+ const initParams = {
+ value: 'initial value',
+ field,
+ } as ICellRendererParams & { field: FieldMetadata };
+
+ renderer.init(initParams);
+
+ const refreshParams = {
+ value: 'updated value',
+ field,
+ } as ICellRendererParams & { field: FieldMetadata };
+
+ const result = renderer.refresh(refreshParams);
+
+ expect(result).toBe(true);
+ });
+
+ it('should refresh with new value for unsupported field type', () => {
+ const field = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'unsupported_type',
+ } as unknown as FieldMetadata;
+
+ const initParams = {
+ value: 'initial value',
+ field,
+ } as ICellRendererParams & { field: FieldMetadata };
+
+ renderer.init(initParams);
+
+ const refreshParams = {
+ value: 'updated value',
+ field,
+ } as ICellRendererParams & { field: FieldMetadata };
+
+ const result = renderer.refresh(refreshParams);
+
+ expect(result).toBe(true);
+ expect(renderer.eGui.textContent).toBe('updated value');
+ });
+
+ it('should clean up root on destroy', () => {
+ const field: FieldMetadata = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'text',
+ };
+
+ const params = {
+ value: 'test',
+ field,
+ } as ICellRendererParams & { field: FieldMetadata };
+
+ renderer.init(params);
+
+ const unmountSpy = vi.spyOn(renderer.root!, 'unmount');
+ renderer.destroy();
+
+ expect(unmountSpy).toHaveBeenCalled();
+ });
+
+ it('should handle destroy when root is null', () => {
+ const field = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'unsupported_type',
+ } as unknown as FieldMetadata;
+
+ const params = {
+ value: 'test',
+ field,
+ } as ICellRendererParams & { field: FieldMetadata };
+
+ renderer.init(params);
+
+ // Should not throw
+ expect(() => renderer.destroy()).not.toThrow();
+ });
+ });
+
+ describe('FieldWidgetCellEditor', () => {
+ let editor: FieldWidgetCellEditor;
+
+ beforeEach(() => {
+ editor = new FieldWidgetCellEditor();
+ });
+
+ afterEach(() => {
+ editor.destroy();
+ });
+
+ it('should initialize with a field widget for supported types', () => {
+ const field: FieldMetadata = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'text',
+ };
+
+ const params = {
+ value: 'initial value',
+ field,
+ } as ICellEditorParams & { field: FieldMetadata };
+
+ editor.init(params);
+
+ expect(editor.eGui).toBeDefined();
+ expect(editor.eGui.className).toBe('field-widget-editor');
+ expect(editor.root).toBeDefined();
+ expect(editor.currentValue).toBe('initial value');
+ });
+
+ it('should initialize with fallback input for unsupported types', () => {
+ const field = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'unsupported_type',
+ } as unknown as FieldMetadata;
+
+ const params = {
+ value: 'fallback value',
+ field,
+ } as ICellEditorParams & { field: FieldMetadata };
+
+ editor.init(params);
+
+ expect(editor.eGui).toBeDefined();
+ expect(editor.root).toBeNull();
+ const input = editor.eGui.querySelector('input');
+ expect(input).toBeDefined();
+ expect(input?.value).toBe('fallback value');
+ });
+
+ it('should return current value', () => {
+ const field: FieldMetadata = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'text',
+ };
+
+ const params = {
+ value: 'test value',
+ field,
+ } as ICellEditorParams & { field: FieldMetadata };
+
+ editor.init(params);
+
+ expect(editor.getValue()).toBe('test value');
+ });
+
+ it('should return the GUI element', () => {
+ const field: FieldMetadata = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'text',
+ };
+
+ const params = {
+ value: 'test',
+ field,
+ } as ICellEditorParams & { field: FieldMetadata };
+
+ editor.init(params);
+ const gui = editor.getGui();
+
+ expect(gui).toBe(editor.eGui);
+ });
+
+ it('should return true for popup editors for specific field types', () => {
+ const popupTypes = ['date', 'datetime', 'select', 'lookup', 'color'] as const;
+
+ popupTypes.forEach(type => {
+ const field: FieldMetadata = {
+ name: 'testField',
+ label: 'Test Field',
+ type,
+ };
+
+ const params = {
+ value: 'test',
+ field,
+ } as ICellEditorParams & { field: FieldMetadata };
+
+ const testEditor = new FieldWidgetCellEditor();
+ testEditor.init(params);
+
+ expect(testEditor.isPopup()).toBe(true);
+ testEditor.destroy();
+ });
+ });
+
+ it('should return false for popup editors for non-popup field types', () => {
+ const field: FieldMetadata = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'text',
+ };
+
+ const params = {
+ value: 'test',
+ field,
+ } as ICellEditorParams & { field: FieldMetadata };
+
+ editor.init(params);
+
+ expect(editor.isPopup()).toBe(false);
+ });
+
+ it('should clean up root on destroy', () => {
+ const field: FieldMetadata = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'text',
+ };
+
+ const params = {
+ value: 'test',
+ field,
+ } as ICellEditorParams & { field: FieldMetadata };
+
+ editor.init(params);
+
+ const unmountSpy = vi.spyOn(editor.root!, 'unmount');
+ editor.destroy();
+
+ expect(unmountSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('createFieldCellRenderer', () => {
+ it('should create a renderer class with field metadata', () => {
+ const field: FieldMetadata = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'text',
+ };
+
+ const RendererClass = createFieldCellRenderer(field);
+ const renderer = new RendererClass();
+
+ const params = {
+ value: 'test value',
+ } as ICellRendererParams;
+
+ renderer.init(params);
+
+ expect(renderer.eGui).toBeDefined();
+ renderer.destroy();
+ });
+ });
+
+ describe('createFieldCellEditor', () => {
+ it('should create an editor class with field metadata', () => {
+ const field: FieldMetadata = {
+ name: 'testField',
+ label: 'Test Field',
+ type: 'text',
+ };
+
+ const EditorClass = createFieldCellEditor(field);
+ const editor = new EditorClass();
+
+ const params = {
+ value: 'test value',
+ } as ICellEditorParams;
+
+ editor.init(params);
+
+ expect(editor.eGui).toBeDefined();
+ editor.destroy();
+ });
+ });
+});
diff --git a/packages/plugin-aggrid/src/field-renderers.tsx b/packages/plugin-aggrid/src/field-renderers.tsx
index 652adb24..b0fa34c5 100644
--- a/packages/plugin-aggrid/src/field-renderers.tsx
+++ b/packages/plugin-aggrid/src/field-renderers.tsx
@@ -38,32 +38,33 @@ import {
/**
* Map field type to field widget component
+ * Defined at module level to avoid recreating on every call
*/
-function getFieldWidget(fieldType: string): React.ComponentType | null {
- const widgetMap: Record> = {
- text: TextField,
- textarea: TextAreaField,
- number: NumberField,
- currency: CurrencyField,
- percent: PercentField,
- boolean: BooleanField,
- select: SelectField,
- date: DateField,
- datetime: DateTimeField,
- time: TimeField,
- email: EmailField,
- phone: PhoneField,
- url: UrlField,
- password: PasswordField,
- color: ColorField,
- rating: RatingField,
- image: ImageField,
- avatar: AvatarField,
- lookup: LookupField,
- slider: SliderField,
- code: CodeField,
- };
+const widgetMap: Record> = {
+ text: TextField,
+ textarea: TextAreaField,
+ number: NumberField,
+ currency: CurrencyField,
+ percent: PercentField,
+ boolean: BooleanField,
+ select: SelectField,
+ date: DateField,
+ datetime: DateTimeField,
+ time: TimeField,
+ email: EmailField,
+ phone: PhoneField,
+ url: UrlField,
+ password: PasswordField,
+ color: ColorField,
+ rating: RatingField,
+ image: ImageField,
+ avatar: AvatarField,
+ lookup: LookupField,
+ slider: SliderField,
+ code: CodeField,
+};
+function getFieldWidget(fieldType: string): React.ComponentType | null {
return widgetMap[fieldType] || null;
}
@@ -117,6 +118,12 @@ export class FieldWidgetCellRenderer {
return true;
}
+ // Fallback to text display when no FieldWidget is available
+ if (this.eGui) {
+ this.eGui.textContent = value != null ? String(value) : '';
+ return true;
+ }
+
return false;
}