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.
- package/.vscode/settings.json +1 -1
- package/package.json +4 -4
- package/source/providers/Pict-Provider-DynamicTabularData.js +204 -17
- package/source/providers/layouts/Pict-Layout-Tabular.js +20 -0
- package/source/services/ManifestFactory.js +15 -3
- package/test/PictSectionForm-Tabular-Features_tests.js +156 -0
package/.vscode/settings.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-section-form",
|
|
3
|
-
"version": "1.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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
|
|
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)', () =>
|