pict-section-form 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  {
2
- "workbench.colorTheme": "Default Dark Modern",
2
+ "workbench.colorTheme": "Default High Contrast",
3
3
  "sqltools.connections": [
4
4
  {
5
5
  "mysqlOptions": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict-section-form",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Pict dynamic form sections",
5
5
  "main": "source/Pict-Section-Form.js",
6
6
  "directories": {
@@ -34,10 +34,10 @@
34
34
  "browser-env": "^3.3.0",
35
35
  "eslint": "^9.39.2",
36
36
  "jquery": "^4.0.0",
37
- "pict": "^1.0.373",
37
+ "pict": "^1.0.386",
38
38
  "pict-application": "^1.0.34",
39
39
  "pict-docuserve": "^1.4.19",
40
- "pict-service-commandlineutility": "^1.0.19",
40
+ "pict-service-commandlineutility": "^1.0.20",
41
41
  "quackage": "^1.3.0",
42
42
  "tui-grid": "^4.21.22",
43
43
  "typescript": "^5.9.3"
@@ -48,7 +48,7 @@
48
48
  "marked": "^4.3.0",
49
49
  "pict-provider": "^1.0.13",
50
50
  "pict-section-excalidraw": "^1.0.3",
51
- "pict-section-markdowneditor": "^1.0.17",
51
+ "pict-section-markdowneditor": "^1.0.19",
52
52
  "pict-section-tuigrid": "^1.0.31",
53
53
  "pict-template": "^1.0.15",
54
54
  "pict-view": "^1.0.68"
@@ -318,13 +318,21 @@ class DynamicTabularData extends libPictProvider
318
318
  this.log.error(`Dynamic View [${pView.UUID}]::[${pView.Hash}] Group ${tmpGroup.Hash} attempting to move row [${pRowIndex}] to [${pNewRowIndex}] but the index is out of bounds.`);
319
319
  return false;
320
320
  }
321
+ let tmpOriginalLength = tmpDestinationObject.length;
321
322
  let tmpElementToBeMoved = tmpDestinationObject.splice(tmpRowIndex, 1);
322
323
  tmpDestinationObject.splice(tmpNewRowIndex, 0, tmpElementToBeMoved[0]);
323
- this.pict.providers.DynamicSolver.solveViews();
324
- pView.render();
325
- //pView.marshalToView();
326
- // We've re-rendered but we don't know what needs to be marshaled based on the solve that ran above so marshal everything
327
- this.pict.views.PictFormMetacontroller.marshalFormSections();
324
+
325
+ // Position-keyed DynamicColumns (KeyBy:"Position") store dependent cell data by the
326
+ // source row's INDEX, so a source reorder must apply the same permutation to every
327
+ // dependent row's positional cells -- otherwise user-entered values stay put while
328
+ // their column re-labels to a different source row. Must run BEFORE the dependent
329
+ // views re-resolve their columns below. No-op for value-keyed generators.
330
+ this._moveDependentPositionalColumns(tmpGroup.RecordSetAddress, tmpRowIndex, tmpNewRowIndex, tmpOriginalLength);
331
+
332
+ // Render (source + dependent views) BEFORE solving so dependent DynamicColumns tables
333
+ // don't blank until the next edit, and the solve's DOM side effects survive. Matches
334
+ // the add/delete handlers' order.
335
+ this._repaintAfterRowReorder(tmpGroup);
328
336
  }
329
337
  }
330
338
  }
@@ -353,13 +361,23 @@ class DynamicTabularData extends libPictProvider
353
361
  this.log.error(`Dynamic View [${pView.UUID}]::[${pView.Hash}] Group ${tmpGroup.Hash} attempting to move row [${pRowIndex}] down but it's already at the bottom.`);
354
362
  return false;
355
363
  }
364
+ let tmpOriginalLength = tmpDestinationObject.length;
356
365
  let tmpElementToBeMoved = tmpDestinationObject.splice(tmpRowIndex, 1);
357
- tmpDestinationObject.splice(tmpRowIndex + 1, 0, tmpElementToBeMoved[0]);
358
- this.pict.providers.DynamicSolver.solveViews();
359
- pView.render();
360
- //pView.marshalToView();
361
- // We've re-rendered but we don't know what needs to be marshaled based on the solve that ran above so marshal everything
362
- this.pict.views.PictFormMetacontroller.marshalFormSections();
366
+ let tmpNewRowIndex = tmpRowIndex + 1;
367
+ tmpDestinationObject.splice(tmpNewRowIndex, 0, tmpElementToBeMoved[0]);
368
+
369
+ // Position-keyed DynamicColumns (KeyBy:"Position") store dependent cell data by the
370
+ // source row's INDEX, so a source reorder must apply the same permutation to every
371
+ // dependent row's positional cells -- otherwise user-entered values stay put while
372
+ // their column re-labels to a different source row. Must run BEFORE the dependent
373
+ // views re-resolve their columns below. No-op for value-keyed generators.
374
+ this._moveDependentPositionalColumns(tmpGroup.RecordSetAddress, tmpRowIndex, tmpNewRowIndex, tmpOriginalLength);
375
+
376
+ // Render (source + dependent views) BEFORE solving so the solve's DOM side effects
377
+ // (e.g. SetGroupVisibility hiding a validation message) act on the freshly rebuilt
378
+ // DOM and survive, and so dependent DynamicColumns tables don't blank until the next
379
+ // edit. Matches the add/delete handlers' order.
380
+ this._repaintAfterRowReorder(tmpGroup);
363
381
  }
364
382
  }
365
383
  }
@@ -393,13 +411,23 @@ class DynamicTabularData extends libPictProvider
393
411
  this.log.error(`Dynamic View [${pView.UUID}]::[${pView.Hash}] Group ${tmpGroup.Hash} attempting to move row [${pRowIndex}] but the index is out of bounds.`);
394
412
  return false;
395
413
  }
414
+ let tmpOriginalLength = tmpDestinationObject.length;
396
415
  let tmpElementToBeMoved = tmpDestinationObject.splice(tmpRowIndex, 1);
397
- tmpDestinationObject.splice(tmpRowIndex - 1, 0, tmpElementToBeMoved[0]);
398
- this.pict.providers.DynamicSolver.solveViews();
399
- pView.render();
400
- //pView.marshalToView();
401
- // We've re-rendered but we don't know what needs to be marshaled based on the solve that ran above so marshal everything
402
- this.pict.views.PictFormMetacontroller.marshalFormSections();
416
+ let tmpNewRowIndex = tmpRowIndex - 1;
417
+ tmpDestinationObject.splice(tmpNewRowIndex, 0, tmpElementToBeMoved[0]);
418
+
419
+ // Position-keyed DynamicColumns (KeyBy:"Position") store dependent cell data by the
420
+ // source row's INDEX, so a source reorder must apply the same permutation to every
421
+ // dependent row's positional cells -- otherwise user-entered values stay put while
422
+ // their column re-labels to a different source row. Must run BEFORE the dependent
423
+ // views re-resolve their columns below. No-op for value-keyed generators.
424
+ this._moveDependentPositionalColumns(tmpGroup.RecordSetAddress, tmpRowIndex, tmpNewRowIndex, tmpOriginalLength);
425
+
426
+ // Render (source + dependent views) BEFORE solving so the solve's DOM side effects
427
+ // (e.g. SetGroupVisibility hiding a validation message) act on the freshly rebuilt
428
+ // DOM and survive, and so dependent DynamicColumns tables don't blank until the next
429
+ // edit. Matches the add/delete handlers' order.
430
+ this._repaintAfterRowReorder(tmpGroup);
403
431
  }
404
432
  }
405
433
  }
@@ -648,6 +676,165 @@ class DynamicTabularData extends libPictProvider
648
676
  }
649
677
  }
650
678
  }
679
+
680
+ /**
681
+ * For position-keyed DynamicColumns (KeyBy: "Position") sourced from
682
+ * pSourceRecordSetAddress, REORDER each dependent row's positional cells to
683
+ * mirror a source row that moved from pOldIndex to pNewIndex. The source array
684
+ * was already spliced (remove at pOldIndex, insert at pNewIndex); this applies
685
+ * the identical permutation to every dependent row's positional cell VALUES so
686
+ * the data stays attached to its column when the columns re-resolve to the new
687
+ * source order. Without it, a reorder leaves user-entered cells under the wrong
688
+ * (renamed) column. Solver-filled rows re-derive on the next solve regardless;
689
+ * applying the move to them too is harmless (the solve overwrites them). No-op
690
+ * for value-keyed generators (their data stays attached to the stable value).
691
+ *
692
+ * Must run AFTER the source splice and BEFORE the dependent views re-resolve +
693
+ * the marshal repaints them. Symmetric with _spliceDependentPositionalColumns.
694
+ *
695
+ * @param {string} pSourceRecordSetAddress - RecordSetAddress of the moved-within source.
696
+ * @param {number} pOldIndex - Source row's index before the move.
697
+ * @param {number} pNewIndex - Source row's index after the move.
698
+ * @param {number} pLength - Source row count (unchanged by a move).
699
+ */
700
+ _moveDependentPositionalColumns(pSourceRecordSetAddress, pOldIndex, pNewIndex, pLength)
701
+ {
702
+ if ((typeof pSourceRecordSetAddress !== 'string') || (pSourceRecordSetAddress.length < 1))
703
+ {
704
+ return;
705
+ }
706
+ if (pOldIndex === pNewIndex)
707
+ {
708
+ return;
709
+ }
710
+ if (!this.pict.views.PictFormMetacontroller)
711
+ {
712
+ return;
713
+ }
714
+ let tmpManifestFactory = this.fable.ManifestFactory;
715
+ if (!tmpManifestFactory || (typeof tmpManifestFactory._parseDynamicColumnTemplate !== 'function'))
716
+ {
717
+ return;
718
+ }
719
+ let tmpViews = this.pict.views.PictFormMetacontroller.filterViews((pViewToTest) => { return pViewToTest.isPictSectionForm; });
720
+ for (let v = 0; v < tmpViews.length; v++)
721
+ {
722
+ let tmpView = tmpViews[v];
723
+ let tmpGroups = tmpView.getGroups();
724
+ for (let g = 0; g < tmpGroups.length; g++)
725
+ {
726
+ let tmpGroup = tmpGroups[g];
727
+ if (!Array.isArray(tmpGroup.DynamicColumns) || (tmpGroup.DynamicColumns.length < 1))
728
+ {
729
+ continue;
730
+ }
731
+ let tmpDependentRows = tmpView.sectionManifest.getValueByHash(tmpView.getMarshalDestinationObject(), tmpGroup.RecordSetAddress);
732
+ if (!Array.isArray(tmpDependentRows) || (tmpDependentRows.length < 1))
733
+ {
734
+ continue;
735
+ }
736
+ for (let c = 0; c < tmpGroup.DynamicColumns.length; c++)
737
+ {
738
+ let tmpGenerator = tmpGroup.DynamicColumns[c];
739
+ if (!tmpGenerator || (tmpGenerator.SourceAddress !== pSourceRecordSetAddress))
740
+ {
741
+ continue;
742
+ }
743
+ // Only position-keyed generators store data by index; value-keyed columns keep
744
+ // their data attached to the (stable) value, so they need no reordering.
745
+ if (tmpGenerator.KeyBy !== 'Position')
746
+ {
747
+ continue;
748
+ }
749
+ if (typeof tmpGenerator.InformaryDataAddressTemplate !== 'string')
750
+ {
751
+ continue;
752
+ }
753
+ // Resolve the positional cell address for each column index once.
754
+ let tmpAddresses = [];
755
+ for (let k = 0; k < pLength; k++)
756
+ {
757
+ tmpAddresses[k] = tmpManifestFactory._parseDynamicColumnTemplate(tmpGenerator.InformaryDataAddressTemplate, { __Index: k, __RowNumber: k + 1 });
758
+ }
759
+ for (let r = 0; r < tmpDependentRows.length; r++)
760
+ {
761
+ let tmpRow = tmpDependentRows[r];
762
+ if (!tmpRow || (typeof tmpRow !== 'object'))
763
+ {
764
+ continue;
765
+ }
766
+ // Read the current positional cell values, apply the identical
767
+ // remove-at-old / insert-at-new permutation, then write them back.
768
+ let tmpValues = [];
769
+ for (let k = 0; k < pLength; k++)
770
+ {
771
+ tmpValues[k] = tmpAddresses[k] ? tmpView.sectionManifest.getValueByHash(tmpRow, tmpAddresses[k]) : undefined;
772
+ }
773
+ let tmpMoved = tmpValues.splice(pOldIndex, 1);
774
+ tmpValues.splice(pNewIndex, 0, tmpMoved[0]);
775
+ for (let k = 0; k < pLength; k++)
776
+ {
777
+ if (tmpAddresses[k])
778
+ {
779
+ tmpView.sectionManifest.setValueByHash(tmpRow, tmpAddresses[k], tmpValues[k]);
780
+ }
781
+ }
782
+ }
783
+ }
784
+ }
785
+ }
786
+ }
787
+
788
+ /**
789
+ * Shared tail for the row-reorder handlers (move up / move down / set index).
790
+ * Repaints the moved source table and every dependent view, in the RENDER phase
791
+ * BEFORE solving + marshaling, then solves and marshals. Mirrors what the add /
792
+ * delete handlers do so a reorder doesn't blank dependent DynamicColumns tables
793
+ * (or the rest of their section) until the next edit. Render happens before the
794
+ * solve so the solve's DOM side effects (e.g. SetGroupVisibility) land on the
795
+ * freshly rebuilt DOM and survive.
796
+ *
797
+ * @param {Object} pGroup - The reordered group (its RecordSetAddress drives dependents).
798
+ */
799
+ _repaintAfterRowReorder(pGroup)
800
+ {
801
+ // Render every view that renders this record set as a table (the source table itself
802
+ // plus any sibling views bound to the same RecordSetAddress).
803
+ let tmpViewsToRender = this.pict.views.PictFormMetacontroller.filterViews(
804
+ (pViewToTestForGroup) =>
805
+ {
806
+ if (!pViewToTestForGroup.isPictSectionForm)
807
+ {
808
+ return false;
809
+ }
810
+ let tmpGroupsToTest = pViewToTestForGroup.getGroups();
811
+ for (let i = 0; i < tmpGroupsToTest.length; i++)
812
+ {
813
+ if (tmpGroupsToTest[i].RecordSetAddress == pGroup.RecordSetAddress)
814
+ {
815
+ return true;
816
+ }
817
+ }
818
+ return false;
819
+ });
820
+ for (let i = 0; i < tmpViewsToRender.length; i++)
821
+ {
822
+ tmpViewsToRender[i].render();
823
+ }
824
+
825
+ // Rebuild any OTHER views whose DynamicColumns are sourced from this record set
826
+ // HERE, in the render phase -- BEFORE the marshal below -- so the column DOM is
827
+ // correct (and re-labeled to the new order) when the marshal fills it. Without
828
+ // this the dependent table is only rebuilt mid-marshal (onDataMarshalToForm),
829
+ // which renders AFTER the cells were filled and blanks them until the next edit.
830
+ this._rebuildDependentDynamicColumnViews(pGroup.RecordSetAddress);
831
+
832
+ // Run the solver
833
+ this.pict.providers.DynamicSolver.solveViews();
834
+
835
+ // We've re-rendered but we don't know what needs to be marshaled based on the solve that ran above so marshal everything
836
+ this.pict.views.PictFormMetacontroller.marshalFormSections();
837
+ }
651
838
  }
652
839
 
653
840
  module.exports = DynamicTabularData;
@@ -1617,6 +1617,8 @@ class TabularLayout extends libPictSectionGroupLayout
1617
1617
  * Loop guard: only triggers a template rebuild + re-render when the
1618
1618
  * desired set of dynamic descriptor hashes ACTUALLY changes from the
1619
1619
  * cached state, so steady state is a no-op (just a row-label recompute).
1620
+ * A label-only change (same columns, a source row renamed) takes the
1621
+ * lighter render()-only path so the header text re-bakes without a rebuild.
1620
1622
  *
1621
1623
  * @param {Object} pView
1622
1624
  * @param {Object} pGroup
@@ -1654,6 +1656,24 @@ class TabularLayout extends libPictSectionGroupLayout
1654
1656
  this._reapplyTabularSelectionHighlights(pView, pGroup);
1655
1657
  return true;
1656
1658
  }
1659
+ if (tmpResult && tmpResult.namesChanged)
1660
+ {
1661
+ // The column SET is unchanged but an existing column's display label was
1662
+ // refreshed (e.g. a source row renamed). _resolveDynamicColumns already
1663
+ // updated the descriptor Name in place; a render() re-bakes the header
1664
+ // labels from it -- no structural template rebuild required.
1665
+ pGroup._RebuildInProgress = true;
1666
+ try
1667
+ {
1668
+ pView.render();
1669
+ }
1670
+ finally
1671
+ {
1672
+ pGroup._RebuildInProgress = false;
1673
+ }
1674
+ this._reapplyTabularSelectionHighlights(pView, pGroup);
1675
+ return true;
1676
+ }
1657
1677
  }
1658
1678
 
1659
1679
  // When the chooser's effective hidden-column set differs from the one the
@@ -289,11 +289,11 @@ class ManifestFactory extends libFableServiceProviderBase
289
289
  *
290
290
  * @param {Object} pView - The view containing the group.
291
291
  * @param {Object} pGroup - The group object (must already have supportingManifest).
292
- * @returns {{added: Array<string>, removed: Array<string>, unchanged: Array<string>, changed: boolean}}
292
+ * @returns {{added: Array<string>, removed: Array<string>, unchanged: Array<string>, changed: boolean, namesChanged: boolean}}
293
293
  */
294
294
  _resolveDynamicColumns(pView, pGroup)
295
295
  {
296
- let tmpEmpty = { added: [], removed: [], unchanged: [], changed: false };
296
+ let tmpEmpty = { added: [], removed: [], unchanged: [], changed: false, namesChanged: false };
297
297
  if (!pGroup || !Array.isArray(pGroup.DynamicColumns) || pGroup.DynamicColumns.length === 0)
298
298
  {
299
299
  return tmpEmpty;
@@ -315,6 +315,7 @@ class ManifestFactory extends libFableServiceProviderBase
315
315
  let tmpAggregateRemoved = [];
316
316
  let tmpAggregateUnchanged = [];
317
317
  let tmpAggregateChanged = false;
318
+ let tmpAggregateNamesChanged = false;
318
319
 
319
320
  for (let i = 0; i < pGroup.DynamicColumns.length; i++)
320
321
  {
@@ -461,6 +462,16 @@ class ManifestFactory extends libFableServiceProviderBase
461
462
  if (tmpExistingDescriptor)
462
463
  {
463
464
  let tmpFreshDescriptor = tmpDesiredDescriptors[tmpAddr];
465
+ // A column whose hash is UNCHANGED can still have a refreshed display label
466
+ // or header group when the source row's name-driving field is edited (e.g.
467
+ // renaming a product). The structural diff above only tracks the hash SET, so
468
+ // flag a label-only change separately -- the layout uses it to re-bake header
469
+ // text via a render() without a full (and costlier) template rebuild.
470
+ if (tmpExistingDescriptor.Name !== tmpFreshDescriptor.Name
471
+ || tmpExistingDescriptor._DynamicColumnHeaderGroup !== tmpFreshDescriptor._DynamicColumnHeaderGroup)
472
+ {
473
+ tmpAggregateNamesChanged = true;
474
+ }
464
475
  tmpExistingDescriptor.Name = tmpFreshDescriptor.Name;
465
476
  tmpExistingDescriptor.DataType = tmpFreshDescriptor.DataType;
466
477
  tmpExistingDescriptor.IsTabular = tmpFreshDescriptor.IsTabular;
@@ -521,7 +532,8 @@ class ManifestFactory extends libFableServiceProviderBase
521
532
  added: tmpAggregateAdded,
522
533
  removed: tmpAggregateRemoved,
523
534
  unchanged: tmpAggregateUnchanged,
524
- changed: tmpAggregateChanged
535
+ changed: tmpAggregateChanged,
536
+ namesChanged: tmpAggregateNamesChanged
525
537
  };
526
538
  }
527
539
 
@@ -568,6 +568,162 @@ suite('PictSectionForm Tabular Features', () =>
568
568
  'new column header resolved from the newly added source row');
569
569
  }, fDone);
570
570
  });
571
+
572
+ test('Moving a source row reorders dependent position-keyed cells (data follows the column)', (fDone) =>
573
+ {
574
+ let App = makeApplication({
575
+ Hash: 'Grades',
576
+ Layout: 'Tabular',
577
+ RecordSetAddress: 'Grades',
578
+ RecordManifest: 'GradeRowEditor',
579
+ DynamicColumns:
580
+ [
581
+ {
582
+ SourceAddress: 'Assignments',
583
+ KeyBy: 'Position',
584
+ HashTemplate: 'PassingCol_{~D:Record.__Index~}',
585
+ NameTemplate: '{~D:Record.Title~}',
586
+ InformaryDataAddressTemplate: 'PassingCol_{~D:Record.__Index~}',
587
+ DataType: 'String',
588
+ PictForm: { InputType: 'Text' }
589
+ }
590
+ ]
591
+ },
592
+ {
593
+ // Each dependent row carries user-entered positional cells (one per source column).
594
+ Grades:
595
+ [
596
+ { Section: 'A', StudentName: 'Alice', PassingCol_0: 'a0', PassingCol_1: 'a1', PassingCol_2: 'a2' },
597
+ { Section: 'A', StudentName: 'Bob', PassingCol_0: 'b0', PassingCol_1: 'b1', PassingCol_2: 'b2' }
598
+ ]
599
+ });
600
+ bootstrap(App, (_Pict) =>
601
+ {
602
+ // Source order is [Addition(0), Photosynthesis(1), Reading 1(2)]. Move row 0 -> 2,
603
+ // the way moveDynamicTableRowDown twice / setDynamicTableRowIndex(0,2) would, then
604
+ // apply the SAME permutation to every dependent row's positional cells.
605
+ _Pict.providers.DynamicTabularData._moveDependentPositionalColumns('Assignments', 0, 2, 3);
606
+
607
+ let tmpRow0 = _Pict.AppData.Grades[0];
608
+ let tmpRow1 = _Pict.AppData.Grades[1];
609
+ // [a0,a1,a2] with source 0->2 becomes [a1,a2,a0] -- the value under each column
610
+ // moved with its source position, so data stays attached to its column.
611
+ Expect(tmpRow0.PassingCol_0).to.equal('a1', 'row 0 col 0 took the value from old position 1');
612
+ Expect(tmpRow0.PassingCol_1).to.equal('a2', 'row 0 col 1 took the value from old position 2');
613
+ Expect(tmpRow0.PassingCol_2).to.equal('a0', 'row 0 col 2 took the moved value from old position 0');
614
+ Expect(tmpRow1.PassingCol_0).to.equal('b1', 'row 1 permuted identically');
615
+ Expect(tmpRow1.PassingCol_1).to.equal('b2', 'row 1 permuted identically');
616
+ Expect(tmpRow1.PassingCol_2).to.equal('b0', 'row 1 permuted identically');
617
+ }, fDone);
618
+ });
619
+
620
+ test('Reordering a source row re-labels dependent columns in the render phase (no blank-out on move)', (fDone) =>
621
+ {
622
+ let App = makeApplication({
623
+ Hash: 'Grades',
624
+ Layout: 'Tabular',
625
+ RecordSetAddress: 'Grades',
626
+ RecordManifest: 'GradeRowEditor',
627
+ DynamicColumns:
628
+ [
629
+ {
630
+ SourceAddress: 'Assignments',
631
+ KeyBy: 'Position',
632
+ HashTemplate: 'PassingCol_{~D:Record.__Index~}',
633
+ NameTemplate: '{~D:Record.Title~}',
634
+ InformaryDataAddressTemplate: 'PassingCol_{~D:Record.__Index~}',
635
+ DataType: 'String',
636
+ PictForm: { InputType: 'Text' }
637
+ }
638
+ ]
639
+ });
640
+ bootstrap(App, (_Pict) =>
641
+ {
642
+ let tmpView = _Pict.views['PictSectionForm-Class'];
643
+ let tmpGroup = tmpView.sectionDefinition.Groups[0];
644
+ Expect(tmpGroup.supportingManifest.elementDescriptors['PassingCol_0'].Name).to.equal('Addition', 'col 0 header before move');
645
+ Expect(tmpGroup.supportingManifest.elementDescriptors['PassingCol_2'].Name).to.equal('Reading 1', 'col 2 header before move');
646
+
647
+ // Reorder the source the way the move handler splices it (move index 0 -> 2), then
648
+ // run the render-phase rebuild the move handler now performs BEFORE marshaling.
649
+ let tmpMoved = _Pict.AppData.Assignments.splice(0, 1);
650
+ _Pict.AppData.Assignments.splice(2, 0, tmpMoved[0]);
651
+ _Pict.providers.DynamicTabularData._rebuildDependentDynamicColumnViews('Assignments');
652
+
653
+ // Column hashes are position-keyed (stable), but their labels must re-resolve to the
654
+ // NEW source order here in the render phase -- not mid-marshal, which would blank the
655
+ // freshly filled cells until the next edit.
656
+ Expect(tmpGroup.supportingManifest.elementDescriptors['PassingCol_0'].Name).to.equal('Photosynthesis', 'col 0 re-labeled to new position 0');
657
+ Expect(tmpGroup.supportingManifest.elementDescriptors['PassingCol_1'].Name).to.equal('Reading 1', 'col 1 re-labeled to new position 1');
658
+ Expect(tmpGroup.supportingManifest.elementDescriptors['PassingCol_2'].Name).to.equal('Addition', 'col 2 re-labeled to the moved row');
659
+ }, fDone);
660
+ });
661
+
662
+ test('Renaming a source row reports namesChanged and refreshes the column header in place', (fDone) =>
663
+ {
664
+ let App = makeApplication({
665
+ Hash: 'Grades',
666
+ Layout: 'Tabular',
667
+ RecordSetAddress: 'Grades',
668
+ RecordManifest: 'GradeRowEditor',
669
+ DynamicColumns:
670
+ [
671
+ {
672
+ SourceAddress: 'Assignments',
673
+ HashTemplate: 'Grade_{~D:Record.IDAssignment~}',
674
+ NameTemplate: '{~D:Record.Title~}',
675
+ InformaryDataAddressTemplate: 'Grades.{~D:Record.IDAssignment~}',
676
+ DataType: 'Number',
677
+ PictForm: { InputType: 'Number' }
678
+ }
679
+ ]
680
+ });
681
+ bootstrap(App, (_Pict) =>
682
+ {
683
+ let tmpView = _Pict.views['PictSectionForm-Class'];
684
+ let tmpGroup = tmpView.sectionDefinition.Groups[0];
685
+ Expect(tmpGroup.supportingManifest.elementDescriptors['Grade_1'].Name).to.equal('Addition', 'baseline header label');
686
+
687
+ // Rename the source row's name-driving field -- no add / remove / reorder, so the
688
+ // column hash set is identical. The header label must still be flagged as changed.
689
+ _Pict.AppData.Assignments[0].Title = 'Renamed';
690
+ let tmpResult = _Pict.ManifestFactory._resolveDynamicColumns(tmpView, tmpGroup);
691
+
692
+ Expect(tmpResult.changed).to.equal(false, 'the column SET did not change (no add/remove/reorder)');
693
+ Expect(tmpResult.namesChanged).to.equal(true, 'an existing column label changed');
694
+ Expect(tmpGroup.supportingManifest.elementDescriptors['Grade_1'].Name).to.equal('Renamed',
695
+ 'descriptor header label refreshed in place so a render() re-bakes it live');
696
+ }, fDone);
697
+ });
698
+
699
+ test('Steady-state re-run reports neither changed nor namesChanged', (fDone) =>
700
+ {
701
+ let App = makeApplication({
702
+ Hash: 'Grades',
703
+ Layout: 'Tabular',
704
+ RecordSetAddress: 'Grades',
705
+ RecordManifest: 'GradeRowEditor',
706
+ DynamicColumns:
707
+ [
708
+ {
709
+ SourceAddress: 'Assignments',
710
+ HashTemplate: 'Grade_{~D:Record.IDAssignment~}',
711
+ NameTemplate: '{~D:Record.Title~}',
712
+ InformaryDataAddressTemplate: 'Grades.{~D:Record.IDAssignment~}',
713
+ DataType: 'Number',
714
+ PictForm: { InputType: 'Number' }
715
+ }
716
+ ]
717
+ });
718
+ bootstrap(App, (_Pict) =>
719
+ {
720
+ let tmpView = _Pict.views['PictSectionForm-Class'];
721
+ let tmpGroup = tmpView.sectionDefinition.Groups[0];
722
+ let tmpResult = _Pict.ManifestFactory._resolveDynamicColumns(tmpView, tmpGroup);
723
+ Expect(tmpResult.changed).to.equal(false, 'no structural change on a steady-state re-run');
724
+ Expect(tmpResult.namesChanged).to.equal(false, 'no false label change on a steady-state re-run');
725
+ }, fDone);
726
+ });
571
727
  });
572
728
 
573
729
  suite('Position-keyed dynamic columns (KeyBy: Position)', () =>