Skip to content

feat: Replace hardcoded cell formatters with @object-ui/fields widgets in object-agrid#391

Open
Copilot wants to merge 5 commits intomainfrom
copilot/update-object-agrid-rendering
Open

feat: Replace hardcoded cell formatters with @object-ui/fields widgets in object-agrid#391
Copilot wants to merge 5 commits intomainfrom
copilot/update-object-agrid-rendering

Conversation

Copy link
Contributor

Copilot AI commented Feb 6, 2026

✅ Implementation Complete: Integrate packages/fields into object-agrid

Objective

Improve object-agrid to use field widget components from packages/fields for rendering cells in both read-only and edit modes.

✅ All Changes Completed

  • Add @object-ui/fields dependency to plugin-aggrid package.json
  • Create field-renderers.tsx with cell renderer and editor utilities
  • Implement custom AG Grid cell renderer that uses field widgets in read-only mode
  • Implement custom AG Grid cell editor that uses field widgets in edit mode
  • Update ObjectAgGridImpl.tsx to use the new cell renderers/editors
  • Clean up unused imports and variables
  • Address initial code review feedback
    • Use explicit Root type from react-dom/client instead of any
    • Use unknown type for currentValue instead of any
    • Replace HTML string renderers with DOM element creation for security
  • NEW: Address PR review feedback
    • Move widgetMap to module level for better performance
    • Fix refresh() to update textContent for fallback case and return true
    • Remove redundant lookup/master_detail cases from fallback switch
    • Add comprehensive test coverage (17 new tests)
  • Build the package successfully (no errors)
  • All tests passing (29/29 tests, including 17 new field-renderer tests)
  • Type checking passes
  • CodeQL security scan passes (0 vulnerabilities)
  • Final validation complete

Technical Implementation

Added Files:

  • packages/plugin-aggrid/src/field-renderers.tsx (221 lines)

    • widgetMap: Module-level constant mapping 21 field types to widgets (performance optimization)
    • getFieldWidget(): Returns widget component for a given field type
    • FieldWidgetCellRenderer: AG Grid cell renderer using field widgets in readonly mode
    • FieldWidgetCellEditor: AG Grid cell editor using field widgets for editing
    • Factory functions for creating renderers/editors with field metadata
    • Type-safe: Uses Root type from react-dom/client
  • packages/plugin-aggrid/src/field-renderers.test.tsx (328 lines)

    • 17 comprehensive tests covering:
      • Widget mapping and fallback behavior
      • Renderer lifecycle (init, refresh, destroy)
      • Editor lifecycle and value handling
      • Memory cleanup (root.unmount())
      • Popup editor configuration
      • Factory function behavior

Modified Files:

  • packages/plugin-aggrid/package.json: Added @object-ui/fields workspace dependency
  • packages/plugin-aggrid/src/ObjectAgGridImpl.tsx:
    • Imported field renderer utilities
    • Replaced 160+ lines of hardcoded formatters with field widgets
    • Removed redundant lookup case (already handled by field widgets)
    • Only master_detail remains in fallback for specialized handling
    • Security: Fallback renderers use DOM element creation instead of HTML strings

Field Types Supported

21 field types with full read-only and edit mode support:

  • Text fields: text, textarea, email, phone, url, password, code
  • Numeric fields: number, currency, percent, slider
  • Date/Time fields: date, datetime, time
  • Selection fields: boolean, select, lookup
  • Visual fields: color, rating, image, avatar

Code Quality Metrics

Type Safety: Explicit types, no any in new code
Security: CodeQL scan passed (0 vulnerabilities)
Testing: 29/29 tests passing (including 17 new tests for field-renderers)
Test Coverage: Comprehensive coverage of renderer/editor lifecycle, fallback, and edge cases
Build: Clean build with no errors
Performance: Module-level widgetMap prevents object recreation on every call
Maintainability: -105 lines of code (replaced hardcoded logic with reusable components)

Performance Improvements

✅ widgetMap defined at module level (avoids recreation on every getFieldWidget call)
✅ Improved refresh() logic returns true to avoid unnecessary DOM manipulation

Test Coverage Summary

New test file: field-renderers.test.tsx

  • FieldWidgetCellRenderer: 10 tests
    • Initialization (supported & unsupported types)
    • Null value handling
    • GUI element access
    • Refresh behavior (widget & fallback modes)
    • Memory cleanup on destroy
  • FieldWidgetCellEditor: 5 tests
    • Initialization (widget & fallback modes)
    • Value handling
    • Popup configuration
    • Memory cleanup
  • Factory functions: 2 tests
    • createFieldCellRenderer
    • createFieldCellEditor

Benefits Achieved

Consistency: All field types now render using the same widgets across forms, grids, and other components
Maintainability: Centralized field rendering logic in @object-ui/fields package
Extensibility: Easy to add new field types - just update the widget map
Security: DOM-based rendering prevents XSS vulnerabilities
Performance: Optimized widget lookup with module-level constant
Quality: Comprehensive test coverage ensures reliability
DX: Type-safe implementation with IntelliSense support
Edit Support: Automatic edit mode for all 21 supported field types

Original prompt

完善object-agrid,应使用packages/fields 中定义的组件来渲染单元格,需要考虑只读和编辑两种模式


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel
Copy link

vercel bot commented Feb 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectui Ready Ready Preview, Comment Feb 6, 2026 5:20am
objectui-console Ready Ready Preview, Comment Feb 6, 2026 5:20am
objectui-storybook Ready Ready Preview, Comment Feb 6, 2026 5:20am

Request Review

Copilot AI and others added 2 commits February 6, 2026 04:48
…ering

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…lements for fallback renderers

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Update object-agrid to use defined components for rendering cells feat: Replace hardcoded cell formatters with @object-ui/fields widgets in object-agrid Feb 6, 2026
Copilot AI requested a review from hotlong February 6, 2026 04:58
@hotlong hotlong marked this pull request as ready for review February 6, 2026 05:01
Copilot AI review requested due to automatic review settings February 6, 2026 05:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request refactors the AG Grid plugin to use field widgets from @object-ui/fields instead of hardcoded cell formatters, achieving consistency across the ObjectUI ecosystem and eliminating ~160 lines of duplicated formatting logic.

Changes:

  • Introduces a unified rendering approach using field widgets for 21 field types (text, number, boolean, date, currency, email, phone, url, color, rating, image, avatar, etc.)
  • Replaces switch-case formatter logic with reusable field widget components that support both read-only and edit modes
  • Improves security by ensuring fallback renderers use DOM creation instead of HTML string concatenation

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 4 comments.

File Description
packages/plugin-aggrid/src/field-renderers.tsx New module implementing AG Grid custom renderers/editors using React field widgets with createRoot, including factory functions for field metadata injection
packages/plugin-aggrid/src/ObjectAgGridImpl.tsx Refactored to use field widget renderers instead of hardcoded formatters; removed ~160 lines of switch-case logic and unused imports; fallback renderers now use safe DOM creation
packages/plugin-aggrid/package.json Added @object-ui/fields workspace dependency
pnpm-lock.yaml Updated lockfile with new dependency and platform-specific binary metadata
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines 456 to 466
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;
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field type 'lookup' appears in both the fieldWidgetTypes array (line 437) and in the fallback switch statement (line 456). This creates redundant code where the lookup type will use the field widget renderer but the fallback case will never be reached. Remove the 'lookup' and 'master_detail' cases from the fallback switch statement since they're already handled by the field widget renderer.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 97ae174. Removed the redundant 'lookup' case from the fallback switch since it's already handled by the field widget renderer in line 437.

);
return true;
}

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refresh() method returns false when there's no FieldWidget for the given field type. According to AG Grid's API, returning false means the cell was not refreshed and AG Grid will remove and re-create the cell. For the fallback case (no widget), you should update the textContent and return true to avoid unnecessary DOM manipulation.

Suggested change
// Fallback to text display when no FieldWidget is available
if (this.eGui) {
this.eGui.textContent = value != null ? String(value) : '';
return true;
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 97ae174. The refresh() method now updates textContent for the fallback case and returns true to avoid unnecessary DOM recreation.

Comment on lines 1 to 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, type Root } 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<any> | null {
const widgetMap: Record<string, React.ComponentType<any>> = {
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: Root | null = null;

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(
<FieldWidget
value={value}
onChange={() => {}} // 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(
<FieldWidget
value={value}
onChange={() => {}} // 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: Root | null = null;
public currentValue: unknown;
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(
<FieldWidget
value={this.currentValue}
onChange={(newValue: any) => {
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 });
}
};
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new field-renderers.tsx module introduces critical rendering and editing logic for AG Grid cells but lacks test coverage. Given that this package has existing test coverage (see index.test.ts and object-aggrid.test.ts), tests should be added for the new field renderer classes. Consider adding tests for: 1) Widget mapping in getFieldWidget(), 2) Renderer lifecycle (init, refresh, destroy), 3) Editor lifecycle and value handling, 4) Fallback behavior when widgets don't exist, and 5) Memory cleanup (root.unmount()).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 97ae174. Added comprehensive test coverage with 17 new tests in field-renderers.test.tsx covering widget mapping, renderer/editor lifecycle, fallback behavior, and memory cleanup. All 29 tests now passing.

Comment on lines 42 to 66
function getFieldWidget(fieldType: string): React.ComponentType<any> | null {
const widgetMap: Record<string, React.ComponentType<any>> = {
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,
};

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getFieldWidget() function recreates the widgetMap object on every call, which is inefficient. The widgetMap should be defined as a module-level constant outside the function to avoid unnecessary object allocations. This will improve performance, especially when rendering large grids with many cells.

Suggested change
function getFieldWidget(fieldType: string): React.ComponentType<any> | null {
const widgetMap: Record<string, React.ComponentType<any>> = {
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<string, React.ComponentType<any>> = {
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<any> | null {

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 97ae174. Moved widgetMap to module level to avoid object recreation on every call, improving performance especially for large grids.

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

📦 Bundle Size Report

Package Size Gzipped
components (index.js) 1775.99KB 419.42KB
core (index.js) 0.63KB 0.30KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 21.35KB 5.35KB
fields (index.js) 87.61KB 17.17KB
layout (index.js) 12.91KB 3.97KB
plugin-aggrid (AddressField--ok8iG9n.js) 3.05KB 0.75KB
plugin-aggrid (AgGridImpl-CPXC7VJh.js) 5.27KB 1.92KB
plugin-aggrid (AutoNumberField-Dv6myrK5.js) 0.28KB 0.27KB
plugin-aggrid (FileField-CH2eulFX.js) 3.50KB 1.34KB
plugin-aggrid (FormulaField-Dpz86irJ.js) 0.52KB 0.39KB
plugin-aggrid (GeolocationField-iCxsaXWf.js) 4.46KB 1.50KB
plugin-aggrid (GridField-BpwWEah1.js) 1.71KB 0.68KB
plugin-aggrid (LocationField-RHwnyYaQ.js) 0.93KB 0.54KB
plugin-aggrid (MasterDetailField-2EBqucp8.js) 3.86KB 1.18KB
plugin-aggrid (ObjectAgGridImpl-BHjZJ3sK.js) 919.75KB 203.54KB
plugin-aggrid (ObjectField-CDkI9ZS2.js) 1.61KB 0.77KB
plugin-aggrid (QRCodeField-yLhaqnlo.js) 3.38KB 1.24KB
plugin-aggrid (RichTextField-S1V_9T8i.js) 1.16KB 0.59KB
plugin-aggrid (SignatureField-DeuxtP8A.js) 3.32KB 1.28KB
plugin-aggrid (SummaryField-W8ABazbp.js) 0.48KB 0.37KB
plugin-aggrid (UserField-8Nfdj90a.js) 2.44KB 0.93KB
plugin-aggrid (VectorField-Oy4OWGBT.js) 0.79KB 0.45KB
plugin-aggrid (index-4NHsUMcZ.js) 19.21KB 4.89KB
plugin-aggrid (index.js) 0.22KB 0.16KB
plugin-calendar (index.js) 31.31KB 8.02KB
plugin-charts (AdvancedChartImpl-DcIHnCct.js) 124.69KB 25.93KB
plugin-charts (BarChart-C_I0OFbj.js) 542.77KB 135.30KB
plugin-charts (ChartImpl-CU5lEzui.js) 3.17KB 1.10KB
plugin-charts (index-DgxI83zT.js) 14.58KB 4.36KB
plugin-charts (index.js) 0.19KB 0.16KB
plugin-chatbot (index.js) 1200.27KB 344.58KB
plugin-dashboard (index.js) 212.71KB 49.09KB
plugin-detail (index.js) 1673.68KB 395.78KB
plugin-editor (MonacoImpl-hfdmoz6k.js) 18.15KB 5.59KB
plugin-editor (index-CuYbY6xb.js) 10.10KB 3.32KB
plugin-editor (index.js) 0.19KB 0.15KB
plugin-form (index.js) 15.35KB 4.96KB
plugin-gantt (index.js) 193.61KB 42.55KB
plugin-grid (index.js) 40.99KB 11.35KB
plugin-kanban (KanbanEnhanced-BqDEu7Z6.js) 31.43KB 9.03KB
plugin-kanban (KanbanImpl-B8nu2BvG.js) 5.28KB 1.91KB
plugin-kanban (index-CrR06na7.js) 17.02KB 4.99KB
plugin-kanban (index.js) 0.27KB 0.17KB
plugin-kanban (sortable.esm-ZHwgFQIO.js) 71.42KB 18.98KB
plugin-list (index.js) 1681.82KB 397.76KB
plugin-map (index.js) 126.03KB 30.49KB
plugin-map (maplibre-gl-CNsW26De.js) 1418.32KB 302.53KB
plugin-markdown (MarkdownImpl-DufQ-eRU.js) 256.68KB 64.45KB
plugin-markdown (index-CrmE78vF.js) 9.63KB 3.17KB
plugin-markdown (index.js) 0.19KB 0.15KB
plugin-timeline (index.js) 105.00KB 24.12KB
plugin-view (index.js) 16.69KB 4.93KB
react (LazyPluginLoader.js) 1.10KB 0.58KB
react (SchemaRenderer.js) 3.28KB 1.30KB
react (index.js) 0.39KB 0.25KB
react (index.test.js) 0.34KB 0.26KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 0.20KB 0.18KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 0.34KB 0.25KB
types (layout.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 0.20KB 0.18KB
types (views.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@hotlong
Copy link
Contributor

hotlong commented Feb 6, 2026

@copilot fix ci build and test

…resh logic, and add comprehensive test coverage

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 6, 2026

@copilot fix ci build and test

CI build and test issues have been fixed in commit 97ae174. All checks now passing:

  • ✅ Build successful (no errors)
  • ✅ All 29 tests passing (including 17 new field-renderer tests)
  • ✅ Type checking passes
  • ✅ No security vulnerabilities

The changes address all code review feedback including performance optimization, improved refresh logic, removal of redundant code, and comprehensive test coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants