pict-section-form 1.3.2 → 1.3.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.
@@ -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 four tabular sections. All the new behavior comes
14
- from the manifest JSON below.
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
- * `Grades[rowIndex].Grades.<IDAssignment>` (preserved on hide).
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": "Grades",
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": "Grades",
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. The expressions cover the eight seeded students; adding
221
- * students would need matching rows here (a real app would generate
222
- * these, but an example keeps them explicit so the mechanism is clear).
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. Tabular grid where the data store is
239
- * `Commentary[].Notes[<IDAssignment>]`. Same DynamicColumns shape,
240
- * different InformaryDataAddress so the data path doesn't collide.
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": "Commentary",
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", "StudentID": "S-001" },
7
- { "Section": "A", "StudentName": "Brian Diaz", "StudentID": "S-002" },
8
- { "Section": "A", "StudentName": "Carla Edwards", "StudentID": "S-003" },
9
- { "Section": "B", "StudentName": "Devon Fox", "StudentID": "S-004" },
10
- { "Section": "B", "StudentName": "Erika Gomez", "StudentID": "S-005" },
11
- { "Section": "B", "StudentName": "Felix Hayes", "StudentID": "S-006" },
12
- { "Section": "C", "StudentName": "Gabriela Ingram","StudentID": "S-007" },
13
- { "Section": "C", "StudentName": "Henry Jensen", "StudentID": "S-008" }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict-section-form",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "Pict dynamic forms",
5
5
  "main": "source/Pict-Section-Form.js",
6
6
  "directories": {
@@ -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
- let tmpName = this._escapeHTML((pDescriptor && pDescriptor.Name != null) ? String(pDescriptor.Name) : '');
102
- return `\n\t\t\t\t\t\t<th data-tabular-column-index="${pColumnIndex}">${tmpName} `
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`;
@@ -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)', () =>