@@ -27,7 +27,13 @@ import { isSystemManagedField, normalizeTableColumnType } from '@object-ui/types
2727import type { I18nLabel } from '@objectstack/spec/ui' ;
2828import { SchemaRenderer , useDataScope , useNavigationOverlay , useAction , useSafeFieldLabel , usePredicateScope , useRelatedRecordActions } from '@object-ui/react' ;
2929import { createSafeTranslation } from '@object-ui/i18n' ;
30- import { getCellRenderer , resolveCellRendererType , formatCurrency , formatCompactCurrency , formatDate , formatPercent , humanizeLabel , getBadgeColorClasses , getBadgeHexAppearance , FieldEditWidget , hasFieldEditWidget , DISCRETE_EDIT_TYPES , coerceToSafeValue } from '@object-ui/fields' ;
30+ // objectui#8920 — the grid reaches a cell renderer through THIS module and
31+ // nowhere else. `getCellRenderer` / `resolveCellRendererType` are deliberately
32+ // NOT imported here: six sites spelling the resolve three different ways is
33+ // what dropped a `format`-hinted column's renderer, and one shared owner is
34+ // what stops a seventh site picking a convention of its own.
35+ import { resolveGridCellRendering , gridCellRendererForFixedKey , BADGE_PREFIX_RENDERER_KEY } from './cellRendererResolution' ;
36+ import { formatCurrency , formatCompactCurrency , formatDate , formatPercent , humanizeLabel , getBadgeColorClasses , getBadgeHexAppearance , FieldEditWidget , hasFieldEditWidget , DISCRETE_EDIT_TYPES , coerceToSafeValue } from '@object-ui/fields' ;
3137import { useLocalization , useDisplayLocale , resolveFieldCurrency } from '@object-ui/i18n' ;
3238import { stateMachineNextValues , isFieldInlineEditable } from './inline-edit-options' ;
3339import {
@@ -2483,7 +2489,8 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
24832489
24842490 // Type-based cell renderer: explicit col type > objectDef type > heuristic inference.
24852491 // Format hints (e.g. `text` + `format: 'phone'`) promote to the
2486- // richer renderer (PhoneCellRenderer) via resolveCellRendererType.
2492+ // richer renderer (PhoneCellRenderer) via the grid's one shared
2493+ // resolve, `./cellRendererResolution` (objectui#8920).
24872494 const objectDefField = objectSchema ?. fields ?. [ col . field ] ;
24882495 // ⭐ ANNOTATED, and the annotation is load-bearing (objectui#6004).
24892496 // `objectSchema` is `useState<any>`, so `objectDefField?.type` is
@@ -2497,10 +2504,11 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
24972504 // object-field fallback below is now the only road, which is what
24982505 // every measured author already used.
24992506 const formatHint = objectDefField ?. format ;
2500- const inferredType : string | null = baseInferredType
2501- ? resolveCellRendererType ( { type : baseInferredType , format : formatHint } )
2502- : null ;
2503- const CellRenderer = inferredType ? getCellRenderer ( inferredType ) : null ;
2507+ // Both answers, from the one shared resolve (objectui#8920):
2508+ // `baseInferredType` is the DECLARED type the inline editor reads,
2509+ // `inferredType` the renderer key it promotes to.
2510+ const { rendererType : inferredType , Renderer } = resolveGridCellRendering ( { type : baseInferredType , format : formatHint } ) ;
2511+ const CellRenderer = inferredType ? Renderer : null ;
25042512
25052513 // Build field metadata for cell renderers with objectDef enrichment
25062514 const fieldMeta : Record < string , any > = { name : col . field , type : inferredType || 'text' } ;
@@ -2636,12 +2644,17 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
26362644 const prefixConfig = col . prefix ;
26372645 if ( prefixConfig ?. field ) {
26382646 const baseCellRenderer = cellRenderer ;
2639- const PrefixRenderer = prefixConfig . type === 'badge' ? getCellRenderer ( 'select' ) : null ;
2647+ // ⭐ The one site whose contract is NOT "declared type + format
2648+ // hint": a FIXED registry key for the badge, owned by this
2649+ // component rather than by the prefixed field (objectui#8920).
2650+ // Named and routed through the same module so it reads as the
2651+ // declared exception it is, not as a fifth silent convention.
2652+ const PrefixRenderer = prefixConfig . type === 'badge' ? gridCellRendererForFixedKey ( BADGE_PREFIX_RENDERER_KEY ) : null ;
26402653 cellRenderer = ( value : any , row : any ) => {
26412654 const prefixValue = row [ prefixConfig . field ] ;
26422655 const prefixEl = prefixValue != null && prefixValue !== ''
26432656 ? PrefixRenderer
2644- ? < PrefixRenderer value = { prefixValue } field = { { name : prefixConfig . field , type : 'select' } as any } />
2657+ ? < PrefixRenderer value = { prefixValue } field = { { name : prefixConfig . field , type : BADGE_PREFIX_RENDERER_KEY } as any } />
26452658 : < span className = "text-muted-foreground text-xs mr-1.5" > { String ( prefixValue ) } </ span >
26462659 : null ;
26472660 return (
@@ -2698,16 +2711,29 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
26982711 const rawHeader = rawFieldLabel || fieldName . charAt ( 0 ) . toUpperCase ( ) + fieldName . slice ( 1 ) . replace ( / _ / g, ' ' ) ;
26992712 const header = schema . objectName ? resolveFieldLabel ( schema . objectName , fieldName , rawHeader ) : rawHeader ;
27002713
2701- // Resolve type: objectDef type > heuristic inference (consistent with ListColumn path)
2702- // Annotated for the same reason as path A's `baseInferredType`
2703- // above: `fieldDef` is `any`, and an `any` reaching the `...(resolvedType
2704- // && { type: resolvedType })` spread below collapses the emit literal
2705- // to `any` (objectui#6004).
2706- const resolvedType : string | null = fieldDef ?. type || inferColumnType ( { field : fieldName } ) || null ;
2707- const CellRenderer = resolvedType ? getCellRenderer ( resolvedType ) : null ;
2714+ // TWO resolves, two names (objectui#8920). "Resolve type" here means
2715+ // objectDef type > heuristic inference — WHICH TYPE THE FIELD HAS,
2716+ // and that is `declaredType`. The published second step, WHICH
2717+ // RENDERER THE TYPE MAPS TO, is `rendererType`; this path used to
2718+ // skip it entirely, so a `text` + `format: 'phone'` column got
2719+ // `TextCellRenderer` and the hint vanished with no diagnostic.
2720+ // A local called `resolvedType` holding only the FIRST answer is the
2721+ // trap that hid that for four of the six sites.
2722+ //
2723+ // The `string | null` annotation path A's `baseInferredType` needs
2724+ // (objectui#6004: `fieldDef` is `any`, and an `any` reaching the
2725+ // `...(declaredType && { type: declaredType })` spread below
2726+ // collapses the emit literal) now lives on `GridCellRendering`'s
2727+ // members — it moved into the helper's return type, it did not go
2728+ // away.
2729+ const { declaredType, rendererType, Renderer } = resolveGridCellRendering ( {
2730+ type : fieldDef ?. type || inferColumnType ( { field : fieldName } ) ,
2731+ format : fieldDef ?. format ,
2732+ } ) ;
2733+ const CellRenderer = rendererType ? Renderer : null ;
27082734
27092735 // Build field metadata with objectDef enrichment
2710- const fieldMeta : Record < string , any > = { name : fieldName , type : resolvedType || 'text' } ;
2736+ const fieldMeta : Record < string , any > = { name : fieldName , type : rendererType || 'text' } ;
27112737 if ( fieldDef ) {
27122738 if ( fieldDef . label ) fieldMeta . label = fieldDef . label ;
27132739 if ( fieldDef . currency ) fieldMeta . currency = fieldDef . currency ;
@@ -2721,16 +2747,16 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
27212747 // reads the schema def directly, see `renderCellEditor` (objectui#7154).
27222748 applyRelationalMeta ( fieldMeta , fieldDef as any ) ;
27232749 // Auto-generate select options from data when no options defined
2724- if ( resolvedType === 'select' && ! fieldMeta . options ) {
2750+ if ( rendererType === 'select' && ! fieldMeta . options ) {
27252751 const uniqueValues = Array . from ( new Set ( data . map ( row => row [ fieldName ] ) . filter ( Boolean ) ) ) ;
27262752 fieldMeta . options = uniqueValues . map ( ( v : any ) => ( { value : v , label : humanizeLabel ( String ( v ) ) } ) ) ;
27272753 }
2728- if ( ( resolvedType === 'select' || resolvedType === 'status' ) && ( fieldDef as any ) ?. appearance != null ) {
2754+ if ( ( rendererType === 'select' || rendererType === 'status' ) && ( fieldDef as any ) ?. appearance != null ) {
27292755 fieldMeta . appearance = ( fieldDef as any ) . appearance ;
27302756 }
27312757
27322758 const numericTypes = [ 'number' , 'currency' , 'percent' ] ;
2733- const inferredAlign = resolvedType && numericTypes . includes ( resolvedType ) ? 'right' as const : undefined ;
2759+ const inferredAlign = rendererType && numericTypes . includes ( rendererType ) ? 'right' as const : undefined ;
27342760
27352761 // Auto-link primary field (first column) to record detail
27362762 const isPrimaryField = colIndex === 0 ;
@@ -2768,9 +2794,12 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
27682794 return {
27692795 header,
27702796 accessorKey : fieldName ,
2771- // Forward the resolved field type for the type-aware inline editor.
2772- ...( resolvedType && { type : resolvedType } ) ,
2773- ...( schema . showColumnTypeIcons && resolvedType && { headerIcon : getTypeIcon ( resolvedType ) } ) ,
2797+ // Forward the DECLARED type for the type-aware inline editor — the
2798+ // renderer key would make a `format`-hinted text column edit as a
2799+ // phone/currency control it never declared. Path A forwards
2800+ // `baseInferredType` for exactly this reason (objectui#8920).
2801+ ...( declaredType && { type : declaredType } ) ,
2802+ ...( schema . showColumnTypeIcons && rendererType && { headerIcon : getTypeIcon ( rendererType ) } ) ,
27742803 ...( inferredAlign && { align : inferredAlign } ) ,
27752804 ...( cellRenderer && { cell : cellRenderer } ) ,
27762805 sortable : fieldDef ?. sortable !== false ,
@@ -2859,13 +2888,18 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
28592888 } ) ;
28602889 return fieldsToShow . map ( ( fieldName ) => {
28612890 const fieldDef = objectSchema ?. fields ?. [ fieldName ] ;
2862- // Annotated for the same reason as paths A and B (objectui#6004).
2863- const resolvedType : string | null = fieldDef ?. type || inferColumnType ( { field : fieldName } ) || null ;
2864- const CellRenderer = resolvedType ? getCellRenderer ( resolvedType ) : null ;
2891+ // The same two resolves as path B, through the same shared owner
2892+ // (objectui#8920) — and the same objectui#6004 annotation, now
2893+ // carried by `GridCellRendering`'s `string | null` members.
2894+ const { declaredType, rendererType, Renderer } = resolveGridCellRendering ( {
2895+ type : fieldDef ?. type || inferColumnType ( { field : fieldName } ) ,
2896+ format : fieldDef ?. format ,
2897+ } ) ;
2898+ const CellRenderer = rendererType ? Renderer : null ;
28652899 const header = fieldDef ?. label || fieldName . charAt ( 0 ) . toUpperCase ( ) + fieldName . slice ( 1 ) . replace ( / _ / g, ' ' ) ;
28662900
28672901 // Build field metadata with objectDef enrichment
2868- const fieldMeta : Record < string , any > = { name : fieldName , type : resolvedType || 'text' } ;
2902+ const fieldMeta : Record < string , any > = { name : fieldName , type : rendererType || 'text' } ;
28692903 if ( fieldDef ) {
28702904 if ( fieldDef . label ) fieldMeta . label = fieldDef . label ;
28712905 if ( fieldDef . currency ) fieldMeta . currency = fieldDef . currency ;
@@ -2879,23 +2913,26 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
28792913 // reads the schema def directly, see `renderCellEditor` (objectui#7154).
28802914 applyRelationalMeta ( fieldMeta , fieldDef as any ) ;
28812915 // Auto-generate select options from data when no options defined
2882- if ( resolvedType === 'select' && ! fieldMeta . options ) {
2916+ if ( rendererType === 'select' && ! fieldMeta . options ) {
28832917 const uniqueValues = Array . from ( new Set ( data . map ( row => row [ fieldName ] ) . filter ( Boolean ) ) ) ;
28842918 fieldMeta . options = uniqueValues . map ( ( v : any ) => ( { value : v , label : humanizeLabel ( String ( v ) ) } ) ) ;
28852919 }
2886- if ( ( resolvedType === 'select' || resolvedType === 'status' ) && ( fieldDef as any ) ?. appearance != null ) {
2920+ if ( ( rendererType === 'select' || rendererType === 'status' ) && ( fieldDef as any ) ?. appearance != null ) {
28872921 fieldMeta . appearance = ( fieldDef as any ) . appearance ;
28882922 }
28892923
28902924 const numericTypes = [ 'number' , 'currency' , 'percent' ] ;
2891- const inferredAlign = resolvedType && numericTypes . includes ( resolvedType ) ? 'right' as const : undefined ;
2925+ const inferredAlign = rendererType && numericTypes . includes ( rendererType ) ? 'right' as const : undefined ;
28922926
28932927 return {
28942928 header,
28952929 accessorKey : fieldName ,
2896- // Forward the resolved field type for the type-aware inline editor.
2897- ...( resolvedType && { type : resolvedType } ) ,
2898- ...( schema . showColumnTypeIcons && resolvedType && { headerIcon : getTypeIcon ( resolvedType ) } ) ,
2930+ // Forward the DECLARED type for the type-aware inline editor — the
2931+ // renderer key would make a `format`-hinted text column edit as a
2932+ // phone/currency control it never declared. Path A forwards
2933+ // `baseInferredType` for exactly this reason (objectui#8920).
2934+ ...( declaredType && { type : declaredType } ) ,
2935+ ...( schema . showColumnTypeIcons && rendererType && { headerIcon : getTypeIcon ( rendererType ) } ) ,
28992936 ...( inferredAlign && { align : inferredAlign } ) ,
29002937 ...( CellRenderer && { cell : ( value : any ) => < CellRenderer value = { value } field = { fieldMeta as any } /> } ) ,
29012938 sortable : fieldDef ?. sortable !== false ,
@@ -2980,9 +3017,12 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
29803017 && ! perms . checkField ( schema . objectName , fieldName , 'read' ) ) return ;
29813018
29823019 // Annotated for the same reason as paths A-C (objectui#6004): `field` is
2983- // `any`, so this value has to be named before it reaches a spread below.
2984- const fieldType : string | undefined = field . type ;
2985- const CellRenderer = getCellRenderer ( field . type ) ;
3020+ // `any`, so these values have to be named before they reach a spread
3021+ // below — the naming now lives on `GridCellRendering`'s `string | null`
3022+ // members. `fieldType` is the DECLARED type the emit forwards to the
3023+ // inline editor; the renderer comes from the `format`-promoted key, which
3024+ // this path used to skip (objectui#8920).
3025+ const { declaredType : fieldType , rendererType, Renderer : CellRenderer } = resolveGridCellRendering ( field ) ;
29863026 const numericTypes = [ 'number' , 'currency' , 'percent' ] ;
29873027 const translatedField = field . options
29883028 ? { ...field , options : translateOptions ( schema . objectName , fieldName , field . options ) }
@@ -2993,7 +3033,9 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
29933033 accessorKey : fieldName ,
29943034 // Forward the field type for the type-aware inline editor.
29953035 ...( fieldType && { type : fieldType } ) ,
2996- ...( numericTypes . includes ( field . type ) && { align : 'right' as const } ) ,
3036+ // Aligned on the RENDERER key, like paths A-C: a `text` column with
3037+ // `format: 'currency'` renders as currency, so it aligns as currency.
3038+ ...( ! ! rendererType && numericTypes . includes ( rendererType ) && { align : 'right' as const } ) ,
29973039 cell : ( value : any ) => < CellRenderer value = { value } field = { fieldForCell } /> ,
29983040 sortable : field . sortable !== false ,
29993041 } ) ;
@@ -4433,11 +4475,17 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
44334475
44344476 // Use objectSchema field type for type-aware rendering
44354477 const fieldDef = objectSchema ?. fields ?. [ key ] ;
4436- if ( fieldDef ?. type ) {
4437- const CellRenderer = getCellRenderer ( fieldDef . type ) ;
4438- if ( CellRenderer ) {
4439- return < CellRenderer value = { value } field = { fieldDef } /> ;
4440- }
4478+ // Through the shared resolve, so the panel honours a `format` hint the
4479+ // same way the row above it does (objectui#8920). `rendererType` is null
4480+ // exactly when the key has no declared type, which is the guard this
4481+ // used to spell as `if (fieldDef?.type)`. The old inner
4482+ // `if (CellRenderer)` was DEAD — `getCellRenderer` ends in
4483+ // `standardMap[key] || TextCellRenderer` and never returns anything
4484+ // falsy — and `GridCellRendering.Renderer` states that totality in the
4485+ // type, so the dead branch goes with it.
4486+ const { rendererType, Renderer } = resolveGridCellRendering ( fieldDef ) ;
4487+ if ( rendererType ) {
4488+ return < Renderer value = { value } field = { fieldDef } /> ;
44414489 }
44424490
44434491 // Fallback: infer from value and key name
0 commit comments