pict-section-form 1.2.2 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict-section-form",
3
- "version": "1.2.2",
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,17 +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
- // Render BEFORE solving so the solve's DOM side effects (e.g. SetGroupVisibility
359
- // hiding a validation message) act on the freshly rebuilt DOM and survive. A
360
- // solve-then-render order discards them, because render() rebuilds the group DOM
361
- // without the solver-applied class. Matches the add/delete handlers' order.
362
- pView.render();
363
- this.pict.providers.DynamicSolver.solveViews();
364
- //pView.marshalToView();
365
- // We've re-rendered but we don't know what needs to be marshaled based on the solve that ran above so marshal everything
366
- 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);
367
381
  }
368
382
  }
369
383
  }
@@ -397,17 +411,23 @@ class DynamicTabularData extends libPictProvider
397
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.`);
398
412
  return false;
399
413
  }
414
+ let tmpOriginalLength = tmpDestinationObject.length;
400
415
  let tmpElementToBeMoved = tmpDestinationObject.splice(tmpRowIndex, 1);
401
- tmpDestinationObject.splice(tmpRowIndex - 1, 0, tmpElementToBeMoved[0]);
402
- // Render BEFORE solving so the solve's DOM side effects (e.g. SetGroupVisibility
403
- // hiding a validation message) act on the freshly rebuilt DOM and survive. A
404
- // solve-then-render order discards them, because render() rebuilds the group DOM
405
- // without the solver-applied class. Matches the add/delete handlers' order.
406
- pView.render();
407
- this.pict.providers.DynamicSolver.solveViews();
408
- //pView.marshalToView();
409
- // We've re-rendered but we don't know what needs to be marshaled based on the solve that ran above so marshal everything
410
- 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);
411
431
  }
412
432
  }
413
433
  }
@@ -656,6 +676,165 @@ class DynamicTabularData extends libPictProvider
656
676
  }
657
677
  }
658
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
+ }
659
838
  }
660
839
 
661
840
  module.exports = DynamicTabularData;
@@ -569,6 +569,96 @@ suite('PictSectionForm Tabular Features', () =>
569
569
  }, fDone);
570
570
  });
571
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
+
572
662
  test('Renaming a source row reports namesChanged and refreshes the column header in place', (fDone) =>
573
663
  {
574
664
  let App = makeApplication({