pict-section-form 1.3.2 → 1.3.4
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/example_applications/gradebook/Gradebook-Application.js +29 -12
- package/example_applications/gradebook/GradebookData.json +8 -28
- package/package.json +1 -1
- package/source/providers/dynamictemplates/Pict-DynamicTemplates-DefaultFormTemplates.js +10 -0
- package/source/providers/layouts/Pict-Layout-Tabular.js +32 -13
- package/test/PictSectionForm-Tabular-Features_tests.js +182 -0
|
@@ -8,10 +8,20 @@
|
|
|
8
8
|
4. The column chooser via Group.ColumnChooser — a "Columns" menu of
|
|
9
9
|
checkboxes above the table; the hidden set lives in the form data
|
|
10
10
|
(<GroupHash>_HiddenColumns) so it survives a save/reload.
|
|
11
|
+
5. ONE record set, many tabular projections. The Students roster, the Grade
|
|
12
|
+
Book, the Performance breakdown and the Commentary grid ALL bind the same
|
|
13
|
+
"Students" array (each tab is just a different column projection of it via
|
|
14
|
+
its own RecordManifest). Because pict-section-form re-renders every tabular
|
|
15
|
+
group that shares a RecordSetAddress when a row is added / removed / moved,
|
|
16
|
+
adding a student on the roster tab makes that student appear on every other
|
|
17
|
+
tab automatically — no glue code, no sync solver. This is the "tabular rows
|
|
18
|
+
automatically map to a shared array" behavior. Per-assignment grades live at
|
|
19
|
+
Students[i].Grades.<IDAssignment> and notes at Students[i].Notes.<IDAssignment>,
|
|
20
|
+
so the dynamic Assignment columns hang off the same row object.
|
|
11
21
|
|
|
12
22
|
The host code itself does nothing interesting — it just wires a TabSectionSelector
|
|
13
|
-
for navigation, then defines
|
|
14
|
-
|
|
23
|
+
for navigation, then defines five tabular sections. All the behavior comes from
|
|
24
|
+
the manifest JSON below.
|
|
15
25
|
*/
|
|
16
26
|
|
|
17
27
|
const libPictSectionForm = require('../../source/Pict-Section-Form.js');
|
|
@@ -108,7 +118,7 @@ module.exports.default_configuration.pict_configuration = (
|
|
|
108
118
|
* - Extra header row (the "Assignments" banner) above the auto super-header.
|
|
109
119
|
* - Dynamic columns: one column per row of `Assignments`, with each
|
|
110
120
|
* descriptor's Name = the assignment title and the data stored at
|
|
111
|
-
* `
|
|
121
|
+
* `Students[rowIndex].Grades.<IDAssignment>` (preserved on hide).
|
|
112
122
|
* - HeaderGroupTemplate auto-extends Headers with a topic super-header
|
|
113
123
|
* row clustering consecutive same-topic columns.
|
|
114
124
|
* - RowSelection / ColumnSelection: check a row and/or a column to
|
|
@@ -131,11 +141,12 @@ module.exports.default_configuration.pict_configuration = (
|
|
|
131
141
|
"Hash": "GradebookGrid",
|
|
132
142
|
"Name": "Grade Book",
|
|
133
143
|
"Layout": "Tabular",
|
|
134
|
-
"RecordSetAddress": "
|
|
144
|
+
"RecordSetAddress": "Students",
|
|
135
145
|
"RecordManifest": "GradeRowEditor",
|
|
136
146
|
"RowSelection": true,
|
|
137
147
|
"ColumnSelection": true,
|
|
138
148
|
"ColumnChooser": true,
|
|
149
|
+
"ColumnSorting": true,
|
|
139
150
|
"Headers":
|
|
140
151
|
[
|
|
141
152
|
[
|
|
@@ -179,7 +190,7 @@ module.exports.default_configuration.pict_configuration = (
|
|
|
179
190
|
"Hash": "PerformanceGrid",
|
|
180
191
|
"Name": "Student Performance Breakdown",
|
|
181
192
|
"Layout": "Tabular",
|
|
182
|
-
"RecordSetAddress": "
|
|
193
|
+
"RecordSetAddress": "Students",
|
|
183
194
|
"RecordManifest": "GradeRowEditor",
|
|
184
195
|
"EditingControlsPosition": "hidden",
|
|
185
196
|
// Recomputes each row's average across its grade values whenever
|
|
@@ -217,9 +228,13 @@ module.exports.default_configuration.pict_configuration = (
|
|
|
217
228
|
/*
|
|
218
229
|
* One colortabularrow() call per student row. Each reads the row's
|
|
219
230
|
* `Average` and maps it to a band color: >=85 green, >=75 amber,
|
|
220
|
-
* else red.
|
|
221
|
-
*
|
|
222
|
-
*
|
|
231
|
+
* else red. These presentational solvers are keyed by row index and
|
|
232
|
+
* cover the eight seeded students. Adding a student still makes the
|
|
233
|
+
* new row appear on every tab (the shared "Students" record set does
|
|
234
|
+
* that automatically) — it just won't be color-banded until a matching
|
|
235
|
+
* row-index solver exists. A real app would generate one per row; the
|
|
236
|
+
* example keeps them explicit so the mechanism stays visible. Out-of-
|
|
237
|
+
* range solvers are a safe no-op, so extra rows simply render unbanded.
|
|
223
238
|
*/
|
|
224
239
|
"Solvers":
|
|
225
240
|
[
|
|
@@ -235,9 +250,11 @@ module.exports.default_configuration.pict_configuration = (
|
|
|
235
250
|
},
|
|
236
251
|
|
|
237
252
|
/*
|
|
238
|
-
* Tab 5 — Teacher commentary.
|
|
239
|
-
* `
|
|
240
|
-
*
|
|
253
|
+
* Tab 5 — Teacher commentary. Another projection of the shared
|
|
254
|
+
* `Students` array; the data store is `Students[].Notes[<IDAssignment>]`.
|
|
255
|
+
* Same DynamicColumns shape as the Grade Book, different
|
|
256
|
+
* InformaryDataAddress (Notes vs Grades) so the two grids hang their
|
|
257
|
+
* per-assignment cells off the same row object without colliding.
|
|
241
258
|
*/
|
|
242
259
|
{
|
|
243
260
|
"Hash": "Commentary",
|
|
@@ -248,7 +265,7 @@ module.exports.default_configuration.pict_configuration = (
|
|
|
248
265
|
"Hash": "CommentaryGrid",
|
|
249
266
|
"Name": "Teacher Commentary",
|
|
250
267
|
"Layout": "Tabular",
|
|
251
|
-
"RecordSetAddress": "
|
|
268
|
+
"RecordSetAddress": "Students",
|
|
252
269
|
"RecordManifest": "CommentaryRowEditor",
|
|
253
270
|
"Headers":
|
|
254
271
|
[
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
"GradebookTab": "Gradebook"
|
|
4
4
|
},
|
|
5
5
|
"Students": [
|
|
6
|
-
{ "Section": "A", "StudentName": "Alice Carter",
|
|
7
|
-
{ "Section": "A", "StudentName": "Brian Diaz",
|
|
8
|
-
{ "Section": "A", "StudentName": "Carla Edwards",
|
|
9
|
-
{ "Section": "B", "StudentName": "Devon Fox",
|
|
10
|
-
{ "Section": "B", "StudentName": "Erika Gomez",
|
|
11
|
-
{ "Section": "B", "StudentName": "Felix Hayes",
|
|
12
|
-
{ "Section": "C", "StudentName": "Gabriela Ingram","StudentID": "S-007" },
|
|
13
|
-
{ "Section": "C", "StudentName": "Henry Jensen",
|
|
6
|
+
{ "Section": "A", "StudentName": "Alice Carter", "StudentID": "S-001", "Average": 89.43, "Grades": { "1": 95, "2": 88, "3": 91, "4": 84, "5": 76, "6": 100, "7": 92 }, "Notes": { "1": "Strong start", "3": "Great essay" } },
|
|
7
|
+
{ "Section": "A", "StudentName": "Brian Diaz", "StudentID": "S-002", "Average": 77.57, "Grades": { "1": 72, "2": 80, "3": 65, "4": 78, "5": 88, "6": 90, "7": 70 }, "Notes": { "5": "Needs practice on plot summary" } },
|
|
8
|
+
{ "Section": "A", "StudentName": "Carla Edwards", "StudentID": "S-003", "Average": 88.71, "Grades": { "1": 88, "2": 85, "3": 92, "4": 90, "5": 95, "6": 85, "7": 86 }, "Notes": {} },
|
|
9
|
+
{ "Section": "B", "StudentName": "Devon Fox", "StudentID": "S-004", "Average": 70.00, "Grades": { "1": 60, "2": 68, "3": 70, "4": 72, "5": 65, "6": 80, "7": 75 }, "Notes": { "1": "Borderline; offer extra worksheets" } },
|
|
10
|
+
{ "Section": "B", "StudentName": "Erika Gomez", "StudentID": "S-005", "Average": 89.57, "Grades": { "1": 90, "2": 92, "3": 85, "4": 88, "5": 89, "6": 95, "7": 88 }, "Notes": {} },
|
|
11
|
+
{ "Section": "B", "StudentName": "Felix Hayes", "StudentID": "S-006", "Average": 77.14, "Grades": { "1": 75, "2": 78, "3": 80, "4": 76, "5": 82, "6": 70, "7": 79 }, "Notes": {} },
|
|
12
|
+
{ "Section": "C", "StudentName": "Gabriela Ingram", "StudentID": "S-007", "Average": 87.43, "Grades": { "1": 82, "2": 90, "3": 88, "4": 85, "5": 91, "6": 92, "7": 84 }, "Notes": { "6": "Beautiful color work" } },
|
|
13
|
+
{ "Section": "C", "StudentName": "Henry Jensen", "StudentID": "S-008", "Average": 68.86, "Grades": { "1": 70, "2": 65, "3": 60, "4": 72, "5": 68, "6": 75, "7": 72 }, "Notes": { "3": "Struggling; schedule one-on-one" } }
|
|
14
14
|
],
|
|
15
15
|
"Assignments": [
|
|
16
16
|
{ "IDAssignment": 1, "Title": "Addition", "Topic": "Math", "Points": 100, "Weight": 1.0 },
|
|
@@ -20,25 +20,5 @@
|
|
|
20
20
|
{ "IDAssignment": 5, "Title": "Reading: A Wrinkle in Time", "Topic": "Reading", "Points": 100, "Weight": 1.0 },
|
|
21
21
|
{ "IDAssignment": 6, "Title": "Watercolor Practice", "Topic": "Art", "Points": 100, "Weight": 0.5 },
|
|
22
22
|
{ "IDAssignment": 7, "Title": "Short Essay", "Topic": "Writing", "Points": 100, "Weight": 1.5 }
|
|
23
|
-
],
|
|
24
|
-
"Grades": [
|
|
25
|
-
{ "Section": "A", "StudentName": "Alice Carter", "Average": 89.43, "Grades": { "1": 95, "2": 88, "3": 91, "4": 84, "5": 76, "6": 100, "7": 92 } },
|
|
26
|
-
{ "Section": "A", "StudentName": "Brian Diaz", "Average": 77.57, "Grades": { "1": 72, "2": 80, "3": 65, "4": 78, "5": 88, "6": 90, "7": 70 } },
|
|
27
|
-
{ "Section": "A", "StudentName": "Carla Edwards", "Average": 88.71, "Grades": { "1": 88, "2": 85, "3": 92, "4": 90, "5": 95, "6": 85, "7": 86 } },
|
|
28
|
-
{ "Section": "B", "StudentName": "Devon Fox", "Average": 70.00, "Grades": { "1": 60, "2": 68, "3": 70, "4": 72, "5": 65, "6": 80, "7": 75 } },
|
|
29
|
-
{ "Section": "B", "StudentName": "Erika Gomez", "Average": 89.57, "Grades": { "1": 90, "2": 92, "3": 85, "4": 88, "5": 89, "6": 95, "7": 88 } },
|
|
30
|
-
{ "Section": "B", "StudentName": "Felix Hayes", "Average": 77.14, "Grades": { "1": 75, "2": 78, "3": 80, "4": 76, "5": 82, "6": 70, "7": 79 } },
|
|
31
|
-
{ "Section": "C", "StudentName": "Gabriela Ingram", "Average": 87.43, "Grades": { "1": 82, "2": 90, "3": 88, "4": 85, "5": 91, "6": 92, "7": 84 } },
|
|
32
|
-
{ "Section": "C", "StudentName": "Henry Jensen", "Average": 68.86, "Grades": { "1": 70, "2": 65, "3": 60, "4": 72, "5": 68, "6": 75, "7": 72 } }
|
|
33
|
-
],
|
|
34
|
-
"Commentary": [
|
|
35
|
-
{ "Section": "A", "StudentName": "Alice Carter", "Notes": { "1": "Strong start", "3": "Great essay" } },
|
|
36
|
-
{ "Section": "A", "StudentName": "Brian Diaz", "Notes": { "5": "Needs practice on plot summary" } },
|
|
37
|
-
{ "Section": "A", "StudentName": "Carla Edwards", "Notes": {} },
|
|
38
|
-
{ "Section": "B", "StudentName": "Devon Fox", "Notes": { "1": "Borderline; offer extra worksheets" } },
|
|
39
|
-
{ "Section": "B", "StudentName": "Erika Gomez", "Notes": {} },
|
|
40
|
-
{ "Section": "B", "StudentName": "Felix Hayes", "Notes": {} },
|
|
41
|
-
{ "Section": "C", "StudentName": "Gabriela Ingram", "Notes": { "6": "Beautiful color work" } },
|
|
42
|
-
{ "Section": "C", "StudentName": "Henry Jensen", "Notes": { "3": "Struggling; schedule one-on-one" } }
|
|
43
23
|
]
|
|
44
24
|
}
|
package/package.json
CHANGED
|
@@ -858,6 +858,16 @@ Glug glug glug Oo... -->
|
|
|
858
858
|
<th data-tabular-column-index="{~D:Record.PictForm.InputIndex~}">{~D:Record.Name~}</th>
|
|
859
859
|
`
|
|
860
860
|
},
|
|
861
|
+
{
|
|
862
|
+
// The column label only (no <th> wrapper). Used by the ColumnSorting
|
|
863
|
+
// header cell so the label resolves dynamically on every render -- the
|
|
864
|
+
// same way the default HeaderCell above does -- instead of being baked
|
|
865
|
+
// as a static string when the template is built. This lets DynamicColumns
|
|
866
|
+
// name changes (e.g. via refreshtabularsection) update the visible header
|
|
867
|
+
// without a full rebuildCustomTemplate.
|
|
868
|
+
"HashPostfix": "-TabularTemplate-HeaderCellLabel",
|
|
869
|
+
"Template": /*HTML*/`{~D:Record.Name~}`
|
|
870
|
+
},
|
|
861
871
|
{
|
|
862
872
|
"HashPostfix": "-TabularTemplate-RowHeader-Postfix",
|
|
863
873
|
"Template": /*HTML*/`
|
|
@@ -98,8 +98,14 @@ class TabularLayout extends libPictSectionGroupLayout
|
|
|
98
98
|
tmpStateClass = 'pict-tabular-sort-desc';
|
|
99
99
|
}
|
|
100
100
|
let tmpGlyph = (typeof this.pict.icon === 'function') ? this.pict.icon(tmpIconName) : '';
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
// Resolve the column label through a metatemplate reference (not a baked
|
|
102
|
+
// literal) so it re-resolves on every render -- this keeps the sortable
|
|
103
|
+
// header in lock-step with the descriptor Name the same way the default
|
|
104
|
+
// (non-sorting) header does. The sort-control glyph/state below is baked,
|
|
105
|
+
// which is fine: it only changes on a sort click, which rebuilds anyway.
|
|
106
|
+
let tmpLabelReference = this.pict.providers.MetatemplateGenerator.getMetatemplateTemplateReference(
|
|
107
|
+
pView, `-TabularTemplate-HeaderCellLabel`, `getTabularRecordInput("${pGroup.GroupIndex}","${pColumnIndex}")`);
|
|
108
|
+
return `\n\t\t\t\t\t\t<th data-tabular-column-index="${pColumnIndex}">${tmpLabelReference} `
|
|
103
109
|
+ `<span class="pict-tabular-sort-control ${tmpStateClass}" `
|
|
104
110
|
+ `onclick="_Pict.providers['Pict-Layout-Tabular'].sortTabularColumn('${pView.Hash}', ${pGroup.GroupIndex}, ${pColumnIndex})">`
|
|
105
111
|
+ `${tmpGlyph}</span></th>\n`;
|
|
@@ -1383,15 +1389,17 @@ class TabularLayout extends libPictSectionGroupLayout
|
|
|
1383
1389
|
// Leading (non-data) columns that precede the data cells: row-select, row labels, left controls.
|
|
1384
1390
|
let tmpLeadingColumnCount = (tmpRowSelectionEnabled ? 1 : 0) + tmpRowLabels.length + (tmpEditingLeft ? 1 : 0);
|
|
1385
1391
|
|
|
1386
|
-
// When EditingControlsPosition isn't 'right' (the default),
|
|
1387
|
-
//
|
|
1388
|
-
//
|
|
1389
|
-
//
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1392
|
+
// When EditingControlsPosition isn't 'right' (the default), the right-side editing-
|
|
1393
|
+
// controls slots are suppressed PER GROUP by gating their emission on tmpEditingRight
|
|
1394
|
+
// below (the RowHeader-ExtraPostfix parity cell and the per-row Row-ExtraPostfix cell).
|
|
1395
|
+
//
|
|
1396
|
+
// We must NOT suppress by registering an empty view-specific override here:
|
|
1397
|
+
// getViewSpecificTemplateHash() is keyed by the view's formsTemplateSetPrefix, which is
|
|
1398
|
+
// shared by EVERY tabular group in the section. A single 'hidden'/'left' group registering
|
|
1399
|
+
// the empty override would shadow the default editing-controls template for sibling groups
|
|
1400
|
+
// that DO want controls ('right'), deleting their menu column from the DOM entirely. The
|
|
1401
|
+
// gate is the group's own EditingControlsPosition, so each group is independent and a
|
|
1402
|
+
// view-specific override (if a host registers one) still applies to the groups that emit.
|
|
1395
1403
|
|
|
1396
1404
|
// The chooser bar sits ABOVE the group's table container (a div can't live
|
|
1397
1405
|
// inside <table> without the browser foster-parenting it out anyway).
|
|
@@ -1536,7 +1544,12 @@ class TabularLayout extends libPictSectionGroupLayout
|
|
|
1536
1544
|
tmpDataColumnCount++;
|
|
1537
1545
|
}
|
|
1538
1546
|
|
|
1539
|
-
|
|
1547
|
+
// Right-side editing-controls header parity cell -- only when this group keeps its
|
|
1548
|
+
// controls on the right. Gated per group (matches the extra-header-row parity above).
|
|
1549
|
+
if (tmpEditingRight)
|
|
1550
|
+
{
|
|
1551
|
+
tmpTemplate += tmpMetatemplateGenerator.getMetatemplateTemplateReference(pView, `-TabularTemplate-RowHeader-ExtraPostfix`, `getGroup("${pGroup.GroupIndex}")`);
|
|
1552
|
+
}
|
|
1540
1553
|
tmpTemplate += tmpMetatemplateGenerator.getMetatemplateTemplateReference(pView, `-TabularTemplate-RowHeader-Postfix`, `getGroup("${pGroup.GroupIndex}")`);
|
|
1541
1554
|
|
|
1542
1555
|
// Warn on header alignment mismatches (data-column count must equal each extra-header
|
|
@@ -1578,7 +1591,13 @@ class TabularLayout extends libPictSectionGroupLayout
|
|
|
1578
1591
|
}
|
|
1579
1592
|
|
|
1580
1593
|
tmpTemplateSetVirtualRowTemplate += `\n\n{~T:${pGroup.SectionTabularRowTemplateHash}:Record~}\n`;
|
|
1581
|
-
|
|
1594
|
+
// Right-side editing-controls cell (del/up/down) -- only when this group keeps its
|
|
1595
|
+
// controls on the right. Gated per group so a sibling group's 'hidden'/'left' setting
|
|
1596
|
+
// can't suppress this group's column. 'left' emits its controls via {~TEC:~} above.
|
|
1597
|
+
if (tmpEditingRight)
|
|
1598
|
+
{
|
|
1599
|
+
tmpTemplateSetVirtualRowTemplate += tmpMetatemplateGenerator.getMetatemplateTemplateReferenceRaw(pView, `-TabularTemplate-Row-ExtraPostfix`, `Record`);
|
|
1600
|
+
}
|
|
1582
1601
|
tmpTemplateSetVirtualRowTemplate += tmpMetatemplateGenerator.getMetatemplateTemplateReference(pView, `-TabularTemplate-Row-Postfix`, `getGroup("${pGroup.GroupIndex}")`);
|
|
1583
1602
|
|
|
1584
1603
|
// This is a custom template expression
|
|
@@ -1288,6 +1288,111 @@ suite('PictSectionForm Tabular Features', () =>
|
|
|
1288
1288
|
Expect(tmpLayout._compareTabularValues(null, 'a')).to.be.lessThan(0);
|
|
1289
1289
|
}, fDone);
|
|
1290
1290
|
});
|
|
1291
|
+
|
|
1292
|
+
// Regression: a ColumnSorting header must keep its label in lock-step with the
|
|
1293
|
+
// descriptor Name. Before the fix, _buildSortableHeaderCell baked the resolved
|
|
1294
|
+
// Name as a static string, so a label change applied by a render-only refresh
|
|
1295
|
+
// (e.g. DynamicColumns recomputed via refreshtabularsection) never reached the
|
|
1296
|
+
// visible header -- only a full rebuildCustomTemplate would. The label is now a
|
|
1297
|
+
// metatemplate reference that re-resolves on every render, matching the default
|
|
1298
|
+
// (non-sorting) header.
|
|
1299
|
+
test('ColumnSorting header label is a dynamic metatemplate reference, not a baked literal', (fDone) =>
|
|
1300
|
+
{
|
|
1301
|
+
let App = makeApplication({
|
|
1302
|
+
Hash: 'Students',
|
|
1303
|
+
Layout: 'Tabular',
|
|
1304
|
+
RecordSetAddress: 'Students',
|
|
1305
|
+
RecordManifest: 'StudentEditor',
|
|
1306
|
+
ColumnSorting: true
|
|
1307
|
+
});
|
|
1308
|
+
bootstrap(App, (_Pict) =>
|
|
1309
|
+
{
|
|
1310
|
+
let tmpView = _Pict.views['PictSectionForm-Class'];
|
|
1311
|
+
let tmpGroup = tmpView.sectionDefinition.Groups[0];
|
|
1312
|
+
let tmpLayout = _Pict.providers['Pict-Layout-Tabular'];
|
|
1313
|
+
let tmpNameIndex = tmpGroup.supportingManifest.elementAddresses.indexOf('StudentName');
|
|
1314
|
+
let tmpDescriptor = tmpGroup.supportingManifest.elementDescriptors['StudentName'];
|
|
1315
|
+
Expect(tmpDescriptor.Name).to.equal('Student', 'baseline descriptor name');
|
|
1316
|
+
|
|
1317
|
+
let tmpHeaderHTML = tmpLayout._buildSortableHeaderCell(tmpView, tmpGroup, tmpDescriptor, tmpNameIndex);
|
|
1318
|
+
// The label is emitted as a metatemplate reference bound to the live descriptor...
|
|
1319
|
+
Expect(tmpHeaderHTML).to.contain('-TabularTemplate-HeaderCellLabel', 'label rendered via the dynamic label template');
|
|
1320
|
+
Expect(tmpHeaderHTML).to.contain(`getTabularRecordInput("${tmpGroup.GroupIndex}","${tmpNameIndex}")`, 'reference bound to this column descriptor');
|
|
1321
|
+
// ...NOT baked as the literal name (which is what caused the stale header).
|
|
1322
|
+
Expect(tmpHeaderHTML).to.not.contain('>Student ', 'descriptor name is not inlined as a static literal');
|
|
1323
|
+
// The sort control is still baked (it only changes on a sort click, which rebuilds).
|
|
1324
|
+
Expect(tmpHeaderHTML).to.contain('pict-tabular-sort-control', 'sort control still present');
|
|
1325
|
+
Expect(tmpHeaderHTML).to.contain('sortTabularColumn', 'sort click handler still wired');
|
|
1326
|
+
}, fDone);
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
test('-TabularTemplate-HeaderCellLabel is registered and resolves the descriptor Name (parity with the default header)', (fDone) =>
|
|
1330
|
+
{
|
|
1331
|
+
let App = makeApplication({
|
|
1332
|
+
Hash: 'Students',
|
|
1333
|
+
Layout: 'Tabular',
|
|
1334
|
+
RecordSetAddress: 'Students',
|
|
1335
|
+
RecordManifest: 'StudentEditor',
|
|
1336
|
+
ColumnSorting: true
|
|
1337
|
+
});
|
|
1338
|
+
bootstrap(App, (_Pict) =>
|
|
1339
|
+
{
|
|
1340
|
+
let tmpView = _Pict.views['PictSectionForm-Class'];
|
|
1341
|
+
// The label template exists under the view's theme prefix and resolves the
|
|
1342
|
+
// same {~D:Record.Name~} data tag the default (non-sorting) HeaderCell uses.
|
|
1343
|
+
let tmpTemplateHash = tmpView.getThemeSpecificTemplateHash('-TabularTemplate-HeaderCellLabel');
|
|
1344
|
+
Expect(tmpTemplateHash in _Pict.TemplateProvider.templates).to.equal(true, 'label template registered');
|
|
1345
|
+
// Template entries are stored as raw template strings.
|
|
1346
|
+
Expect(_Pict.TemplateProvider.templates[tmpTemplateHash]).to.contain('{~D:Record.Name~}', 'label resolves the descriptor Name dynamically');
|
|
1347
|
+
}, fDone);
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
test('ColumnSorting header reflects a descriptor Name change on render() without a rebuildCustomTemplate', (fDone) =>
|
|
1351
|
+
{
|
|
1352
|
+
let App = makeApplication({
|
|
1353
|
+
Hash: 'Students',
|
|
1354
|
+
Layout: 'Tabular',
|
|
1355
|
+
RecordSetAddress: 'Students',
|
|
1356
|
+
RecordManifest: 'StudentEditor',
|
|
1357
|
+
ColumnSorting: true
|
|
1358
|
+
});
|
|
1359
|
+
bootstrap(App, (_Pict) =>
|
|
1360
|
+
{
|
|
1361
|
+
let tmpView = _Pict.views['PictSectionForm-Class'];
|
|
1362
|
+
let tmpGroup = tmpView.sectionDefinition.Groups[0];
|
|
1363
|
+
// Target the StudentName column specifically (the StudentEditor manifest
|
|
1364
|
+
// also has a Section column at index 0).
|
|
1365
|
+
let tmpNameIndex = tmpGroup.supportingManifest.elementAddresses.indexOf('StudentName');
|
|
1366
|
+
let tmpHeaderSelector = `th[data-tabular-column-index="${tmpNameIndex}"]`;
|
|
1367
|
+
|
|
1368
|
+
// Render the section into its real destination so we can read the baked header.
|
|
1369
|
+
let tmpContainerId = String(tmpView.sectionDefinition.DefaultDestinationAddress || '').replace('#', '');
|
|
1370
|
+
let tmpContainer = window.document.createElement('div');
|
|
1371
|
+
tmpContainer.id = tmpContainerId;
|
|
1372
|
+
window.document.body.appendChild(tmpContainer);
|
|
1373
|
+
try
|
|
1374
|
+
{
|
|
1375
|
+
tmpView.render();
|
|
1376
|
+
let tmpHeader = tmpContainer.querySelector(tmpHeaderSelector);
|
|
1377
|
+
Expect(tmpHeader).to.not.equal(null, 'a sortable header cell rendered');
|
|
1378
|
+
Expect(tmpHeader.textContent).to.contain('Student', 'header shows the original descriptor name');
|
|
1379
|
+
|
|
1380
|
+
// Simulate what a render-only refresh (refreshtabularsection / DynamicColumns
|
|
1381
|
+
// name recompute) does: the live descriptor's Name changes, then render() runs
|
|
1382
|
+
// WITHOUT rebuildCustomTemplate(). The header must follow.
|
|
1383
|
+
tmpGroup.supportingManifest.elementDescriptors['StudentName'].Name = 'Aggregate Base';
|
|
1384
|
+
tmpView.render();
|
|
1385
|
+
|
|
1386
|
+
let tmpHeaderAfter = tmpContainer.querySelector(tmpHeaderSelector);
|
|
1387
|
+
Expect(tmpHeaderAfter.textContent).to.contain('Aggregate Base', 'header updated to the new name on render alone');
|
|
1388
|
+
Expect(tmpHeaderAfter.textContent).to.not.contain('Student', 'stale label no longer shown');
|
|
1389
|
+
}
|
|
1390
|
+
finally
|
|
1391
|
+
{
|
|
1392
|
+
window.document.body.removeChild(tmpContainer);
|
|
1393
|
+
}
|
|
1394
|
+
}, fDone);
|
|
1395
|
+
});
|
|
1291
1396
|
});
|
|
1292
1397
|
|
|
1293
1398
|
suite('Tabular column chooser (ColumnChooser)', () =>
|
|
@@ -1582,4 +1687,81 @@ suite('PictSectionForm Tabular Features', () =>
|
|
|
1582
1687
|
}, fDone);
|
|
1583
1688
|
});
|
|
1584
1689
|
});
|
|
1690
|
+
|
|
1691
|
+
// ------------------------------------------------------------------------
|
|
1692
|
+
// Regression: editing controls are PER GROUP, not per section.
|
|
1693
|
+
// A 'hidden'/'left' tabular group used to register a view-shared empty
|
|
1694
|
+
// template override (keyed by formsTemplateSetPrefix, which every group in
|
|
1695
|
+
// the section shares), which deleted the right-side controls column of any
|
|
1696
|
+
// sibling 'right' group -- the column was absent from the DOM, not just
|
|
1697
|
+
// hidden. The fix gates the controls emission on the group's own
|
|
1698
|
+
// EditingControlsPosition instead of a shared override.
|
|
1699
|
+
// ------------------------------------------------------------------------
|
|
1700
|
+
suite('Editing controls position is per-group (no section-wide bleed)', () =>
|
|
1701
|
+
{
|
|
1702
|
+
function makeTwoGroupApplication(pGroupA, pGroupB)
|
|
1703
|
+
{
|
|
1704
|
+
let App = makeApplication(pGroupA);
|
|
1705
|
+
App.default_configuration.pict_configuration.DefaultFormManifest.Sections[0].Groups.push(JSON.parse(JSON.stringify(pGroupB)));
|
|
1706
|
+
return App;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
test("A sibling 'hidden' group does not suppress a 'right' group's controls column", (fDone) =>
|
|
1710
|
+
{
|
|
1711
|
+
let App = makeTwoGroupApplication(
|
|
1712
|
+
{ Hash: 'Students', Layout: 'Tabular', RecordSetAddress: 'Students', RecordManifest: 'StudentEditor', EditingControlsPosition: 'hidden' },
|
|
1713
|
+
{ Hash: 'Grades', Layout: 'Tabular', RecordSetAddress: 'Grades', RecordManifest: 'GradeRowEditor', EditingControlsPosition: 'right' }
|
|
1714
|
+
);
|
|
1715
|
+
bootstrap(App, (_Pict) =>
|
|
1716
|
+
{
|
|
1717
|
+
let tmpView = _Pict.views['PictSectionForm-Class'];
|
|
1718
|
+
let tmpLayout = _Pict.providers['Pict-Layout-Tabular'];
|
|
1719
|
+
let tmpHidden = tmpView.sectionDefinition.Groups[0];
|
|
1720
|
+
let tmpRight = tmpView.sectionDefinition.Groups[1];
|
|
1721
|
+
|
|
1722
|
+
// Bake in section order -- the 'hidden' group first, which is what used to
|
|
1723
|
+
// poison the section by registering the view-shared empty override.
|
|
1724
|
+
let tmpHiddenTemplate = tmpLayout.generateGroupLayoutTemplate(tmpView, tmpHidden);
|
|
1725
|
+
let tmpRightTemplate = tmpLayout.generateGroupLayoutTemplate(tmpView, tmpRight);
|
|
1726
|
+
|
|
1727
|
+
// (1) ROOT-CAUSE GUARD: a hidden/left group must NOT register a section-shared
|
|
1728
|
+
// empty override at the view-specific prefix (this is what shadowed the default
|
|
1729
|
+
// controls template for every other group in the section).
|
|
1730
|
+
Expect(tmpView.checkViewSpecificTemplate('-TabularTemplate-Row-ExtraPostfix')).to.equal(false, 'no section-shared per-row controls override registered');
|
|
1731
|
+
Expect(tmpView.checkViewSpecificTemplate('-TabularTemplate-RowHeader-ExtraPostfix')).to.equal(false, 'no section-shared header controls override registered');
|
|
1732
|
+
|
|
1733
|
+
// (2) The 'hidden' group itself emits no right-side controls (header parity or row cell).
|
|
1734
|
+
Expect(tmpHidden.SectionTabularRowVirtualTemplateHash).to.be.a('string');
|
|
1735
|
+
let tmpHiddenRow = _Pict.TemplateProvider.templates[tmpHidden.SectionTabularRowVirtualTemplateHash];
|
|
1736
|
+
Expect(tmpHiddenTemplate).to.not.contain('-TabularTemplate-RowHeader-ExtraPostfix', 'hidden group emits no header controls cell');
|
|
1737
|
+
Expect(tmpHiddenRow).to.not.contain('-TabularTemplate-Row-ExtraPostfix', 'hidden group emits no per-row controls cell');
|
|
1738
|
+
|
|
1739
|
+
// (3) The 'right' sibling KEEPS its controls (header parity + per-row cell).
|
|
1740
|
+
Expect(tmpRight.SectionTabularRowVirtualTemplateHash).to.be.a('string');
|
|
1741
|
+
let tmpRightRow = _Pict.TemplateProvider.templates[tmpRight.SectionTabularRowVirtualTemplateHash];
|
|
1742
|
+
Expect(tmpRightTemplate).to.contain('-TabularTemplate-RowHeader-ExtraPostfix', 'right group keeps its header controls cell');
|
|
1743
|
+
Expect(tmpRightRow).to.contain('-TabularTemplate-Row-ExtraPostfix', 'right group keeps its per-row controls cell');
|
|
1744
|
+
|
|
1745
|
+
// (4) ...and that per-row reference resolves to the REAL del/up/down control
|
|
1746
|
+
// (the default template is intact and reachable, not an empty override).
|
|
1747
|
+
let tmpDefaultControl = _Pict.TemplateProvider.templates[tmpView.defaultTemplatePrefix + '-TabularTemplate-Row-ExtraPostfix'];
|
|
1748
|
+
Expect(tmpDefaultControl).to.contain('deleteDynamicTableRow', 'default row-controls template still wired to the delete/move handlers');
|
|
1749
|
+
}, fDone);
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
test("A lone 'right' (default) group keeps its controls column", (fDone) =>
|
|
1753
|
+
{
|
|
1754
|
+
let App = makeApplication({ Hash: 'Students', Layout: 'Tabular', RecordSetAddress: 'Students', RecordManifest: 'StudentEditor' });
|
|
1755
|
+
bootstrap(App, (_Pict) =>
|
|
1756
|
+
{
|
|
1757
|
+
let tmpView = _Pict.views['PictSectionForm-Class'];
|
|
1758
|
+
let tmpLayout = _Pict.providers['Pict-Layout-Tabular'];
|
|
1759
|
+
let tmpGroup = tmpView.sectionDefinition.Groups[0];
|
|
1760
|
+
let tmpTemplate = tmpLayout.generateGroupLayoutTemplate(tmpView, tmpGroup);
|
|
1761
|
+
let tmpRow = _Pict.TemplateProvider.templates[tmpGroup.SectionTabularRowVirtualTemplateHash];
|
|
1762
|
+
Expect(tmpTemplate).to.contain('-TabularTemplate-RowHeader-ExtraPostfix', 'default (right) group has its header controls cell');
|
|
1763
|
+
Expect(tmpRow).to.contain('-TabularTemplate-Row-ExtraPostfix', 'default (right) group has its per-row controls cell');
|
|
1764
|
+
}, fDone);
|
|
1765
|
+
});
|
|
1766
|
+
});
|
|
1585
1767
|
});
|