@@ -589,3 +589,83 @@ describe('[#19289] validateFieldConsumers — a `user` field displays a field on
589589 expect ( run ) . toThrow ( / ` r e f e r e n c e ` i s a n o b j e c t / ) ;
590590 } ) ;
591591} ) ;
592+
593+ /**
594+ * [#20929] An inline grid column's `name` names a field of the CHILD object.
595+ *
596+ * `name` is a `LITERAL_KEYS` literal, so the general walk never read a column's
597+ * `name`, and the recommended identity-only column (`{ name: 'qty' }`) says
598+ * nothing else. `os validate` then warned that a field the grid draws was
599+ * inert. The fix reads `name` at that one position, against the child object
600+ * each carrier resolves. It does not drop `name` from the literals.
601+ *
602+ * One fixture carries every assertion. `inv` is the parent and `line` the
603+ * child, related by `line.invoice`. On `line`, `qty` is named only by the grid
604+ * column under test, and `memo` is named nowhere: `memo` is the control, still
605+ * reported. The parent declares a `qty` of its own that nothing reads, so it is
606+ * reported too. A column credited to the wrong object shows up as `inv.qty`
607+ * going quiet while `line.qty` stays reported.
608+ */
609+ describe ( '[#20929] validateFieldConsumers — an inline grid column names a field of the CHILD object' , ( ) => {
610+ const data = { provider : 'object' , object : 'inv' } ;
611+ const columns = [ { name : 'qty' } ] ;
612+
613+ const stack = ( relationship : AnyRec , view : AnyRec = { } , extra : AnyRec = { } ) : AnyRec => ( {
614+ objects : [
615+ { name : 'inv' , fields : { name : { type : 'text' } , qty : { type : 'number' } } } ,
616+ {
617+ name : 'line' ,
618+ fields : {
619+ name : { type : 'text' } ,
620+ invoice : { type : 'master_detail' , reference : 'inv' , ...relationship } ,
621+ qty : { type : 'number' } ,
622+ memo : { type : 'text' } ,
623+ } ,
624+ } ,
625+ ] ,
626+ views : [ { list : { type : 'grid' , data, columns : [ { field : 'name' } ] } , ...view } ] ,
627+ ...extra ,
628+ } ) ;
629+
630+ /** `object.field` → verdict, for every field the rule reports. */
631+ const verdicts = ( s : AnyRec ) : Record < string , string > =>
632+ Object . fromEntries ( validateFieldConsumers ( s ) . map ( ( f ) => [ `${ f . object } .${ f . field } ` , f . verdict ] ) ) ;
633+
634+ /** The child's `qty` credited; the parent's `qty` and the control still reported. */
635+ const CREDITED = { 'inv.qty' : 'inert' , 'line.memo' : 'inert' } ;
636+
637+ it ( 'baseline: with no grid anywhere, all three fields are reported inert' , ( ) => {
638+ expect ( verdicts ( stack ( { } ) ) ) . toEqual ( { 'inv.qty' : 'inert' , 'line.qty' : 'inert' , 'line.memo' : 'inert' } ) ;
639+ } ) ;
640+
641+ it ( "a relationship field's `inlineColumns`: the child is the object that DECLARES the field, not the related one" , ( ) => {
642+ expect ( verdicts ( stack ( { inlineEdit : 'grid' , inlineColumns : columns } ) ) ) . toEqual ( CREDITED ) ;
643+ } ) ;
644+
645+ it ( "a form view's `subforms[].columns`: the child is the entry's `childObject`, not the view's object" , ( ) => {
646+ const form = { type : 'simple' , data, subforms : [ { childObject : 'line' , columns } ] } ;
647+ expect ( verdicts ( stack ( { } , { form } ) ) ) . toEqual ( CREDITED ) ;
648+ } ) ;
649+
650+ it ( "each `formViews` entry's `subforms[].columns`, the same way" , ( ) => {
651+ const edit = { type : 'simple' , data, subforms : [ { childObject : 'line' , columns } ] } ;
652+ expect ( verdicts ( stack ( { } , { formViews : { edit } } ) ) ) . toEqual ( CREDITED ) ;
653+ } ) ;
654+
655+ it ( '`inlineColumns` on a field that does not set `inlineEdit` draws no grid: a carrier, listed for removal' , ( ) => {
656+ const findings = validateFieldConsumers ( stack ( { inlineColumns : columns } ) ) ;
657+ expect ( Object . fromEntries ( findings . map ( ( f ) => [ `${ f . object } .${ f . field } ` , f . verdict ] ) ) ) . toEqual ( {
658+ 'inv.qty' : 'inert' ,
659+ 'line.qty' : 'carrier-only' ,
660+ 'line.memo' : 'inert' ,
661+ } ) ;
662+ expect ( findings . find ( ( f ) => f . object === 'line' && f . field === 'qty' ) ?. carriers ) . toEqual ( [
663+ 'objects[1].fields.invoice.inlineColumns[0].name' ,
664+ ] ) ;
665+ } ) ;
666+
667+ it ( '`name` anywhere else stays a literal: a dataset measure named like the field credits nothing' , ( ) => {
668+ const datasets = [ { name : 'line_stats' , object : 'line' , measures : [ { name : 'qty' , aggregate : 'count' } ] } ] ;
669+ expect ( verdicts ( stack ( { } , { } , { datasets } ) ) ) . toEqual ( { 'inv.qty' : 'inert' , 'line.qty' : 'inert' , 'line.memo' : 'inert' } ) ;
670+ } ) ;
671+ } ) ;
0 commit comments