pict-section-recordset 1.9.5 → 1.10.0
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/README.md +38 -0
- package/package.json +1 -1
- package/source/Pict-Section-RecordSet.js +1 -0
- package/source/providers/Column-Data-Provider.js +219 -0
- package/source/providers/RecordSet-RecordProvider-Base.js +51 -0
- package/source/providers/RecordSet-RecordProvider-MeadowEndpoints.js +55 -3
- package/source/services/RecordsSet-MetaController.js +23 -1
- package/source/templates/Pict-Template-FilterInstanceViews.js +4 -0
- package/source/views/RecordSet-Filters.js +54 -1
- package/source/views/list/RecordSet-List-ColumnChooser.js +345 -0
- package/source/views/list/RecordSet-List-RecordListEntry.js +4 -1
- package/source/views/list/RecordSet-List.js +412 -14
- package/source/views/read/RecordSet-Read.js +65 -6
- package/types/Pict-Section-RecordSet.d.ts +1 -0
- package/types/providers/Column-Data-Provider.d.ts +115 -0
- package/types/providers/Column-Data-Provider.d.ts.map +1 -0
- package/types/providers/RecordSet-DynamicRecordsetSolver.d.ts +3 -0
- package/types/providers/RecordSet-DynamicRecordsetSolver.d.ts.map +1 -1
- package/types/providers/RecordSet-RecordProvider-Base.d.ts +110 -0
- package/types/providers/RecordSet-RecordProvider-Base.d.ts.map +1 -1
- package/types/providers/RecordSet-RecordProvider-MeadowEndpoints.d.ts +51 -1
- package/types/providers/RecordSet-RecordProvider-MeadowEndpoints.d.ts.map +1 -1
- package/types/providers/RecordSet-Router.d.ts +1 -0
- package/types/providers/RecordSet-Router.d.ts.map +1 -1
- package/types/services/RecordsSet-MetaController.d.ts.map +1 -1
- package/types/templates/Pict-Template-FilterInstanceViews.d.ts.map +1 -1
- package/types/views/RecordSet-Filters.d.ts +61 -0
- package/types/views/RecordSet-Filters.d.ts.map +1 -1
- package/types/views/RecordSet-RecordBaseView.d.ts +1 -0
- package/types/views/RecordSet-RecordBaseView.d.ts.map +1 -1
- package/types/views/filters/RecordSet-Filter-EntityReference-Base.d.ts.map +1 -1
- package/types/views/list/RecordSet-List-ColumnChooser.d.ts +68 -0
- package/types/views/list/RecordSet-List-ColumnChooser.d.ts.map +1 -0
- package/types/views/list/RecordSet-List-RecordListEntry.d.ts.map +1 -1
- package/types/views/list/RecordSet-List.d.ts +167 -2
- package/types/views/list/RecordSet-List.d.ts.map +1 -1
- package/types/views/read/RecordSet-Read.d.ts +8 -0
- package/types/views/read/RecordSet-Read.d.ts.map +1 -1
|
@@ -8,6 +8,7 @@ const viewRecordList = require('./RecordSet-List-RecordList.js');
|
|
|
8
8
|
const viewRecordListHeader = require('./RecordSet-List-RecordListHeader.js');
|
|
9
9
|
const viewRecordListEntry = require('./RecordSet-List-RecordListEntry.js');
|
|
10
10
|
const viewPaginationBottom = require('./RecordSet-List-PaginationBottom.js');
|
|
11
|
+
const viewColumnChooser = require('./RecordSet-List-ColumnChooser.js');
|
|
11
12
|
|
|
12
13
|
/** @type {Record<string, any>} */
|
|
13
14
|
const _DEFAULT_CONFIGURATION__List = (
|
|
@@ -32,10 +33,18 @@ const _DEFAULT_CONFIGURATION__List = (
|
|
|
32
33
|
AutoSolveOrdinal: 0,
|
|
33
34
|
|
|
34
35
|
CSS: /*css*/`
|
|
35
|
-
.prsp-list-loading {
|
|
36
|
-
.prsp-list-loading-inner { display: inline-flex; align-items: center; gap: 0.
|
|
36
|
+
.prsp-list-loading { width: 100%; padding: 0.25rem 0 0.5rem; }
|
|
37
|
+
.prsp-list-loading-inner { display: inline-flex; align-items: center; gap: 0.55em; color: var(--theme-color-text-muted, #64748b); font-size: 1rem; padding: 0.5rem 0.25rem 0.7rem; }
|
|
37
38
|
.prsp-list-spinner { display: inline-flex; animation: prsp-list-spin 0.9s linear infinite; }
|
|
38
39
|
@keyframes prsp-list-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
40
|
+
/* Skeleton ghost rows: a few light, theme-colored bars that fill the loading area so the preserved
|
|
41
|
+
row height doesn't read as a white void. A bottom fade blends the last rows into the page. */
|
|
42
|
+
.prsp-list-skeleton { position: relative; animation: prsp-list-skeleton-pulse 1.8s ease-in-out infinite; }
|
|
43
|
+
@keyframes prsp-list-skeleton-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } }
|
|
44
|
+
.prsp-list-skeleton-row { height: 2.6rem; margin: 0 0 0.7rem; border-radius: 8px; background: var(--theme-color-background-tertiary, #eceef2); }
|
|
45
|
+
.prsp-list-skeleton-row:nth-child(4n) { width: 94%; }
|
|
46
|
+
.prsp-list-skeleton-row:nth-child(5n) { width: 97%; }
|
|
47
|
+
.prsp-list-skeleton-fade { position: absolute; left: 0; right: 0; bottom: 0; height: 4.5rem; background: linear-gradient(to bottom, transparent, var(--theme-color-background-primary, #fff)); pointer-events: none; }
|
|
39
48
|
`,
|
|
40
49
|
CSSPriority: 500,
|
|
41
50
|
|
|
@@ -51,6 +60,7 @@ const _DEFAULT_CONFIGURATION__List = (
|
|
|
51
60
|
<section id="PRSP_Filters_Container">
|
|
52
61
|
{~FV:PRSP-Filters:List~}
|
|
53
62
|
</section>
|
|
63
|
+
<div id="PRSP_ColumnChooser_Container">{~V:PRSP-List-ColumnChooser~}</div>
|
|
54
64
|
<div id="PRSP_PaginationTop_Container">{~V:PRSP-List-PaginationTop~}</div>
|
|
55
65
|
<div id="PRSP_RecordList_Container">{~V:PRSP-List-RecordList~}</div>
|
|
56
66
|
<div id="PRSP_PaginationBottom_Container">{~V:PRSP-List-PaginationBottom~}</div>
|
|
@@ -67,13 +77,21 @@ const _DEFAULT_CONFIGURATION__List = (
|
|
|
67
77
|
{
|
|
68
78
|
Hash: 'PRSP-List-LoadingShell',
|
|
69
79
|
Template: /*html*/`
|
|
70
|
-
<
|
|
80
|
+
<div id="PRSP_List_Loading" class="prsp-list-loading">
|
|
71
81
|
<div class="prsp-list-loading-inner">
|
|
72
82
|
<span class="prsp-list-spinner" aria-hidden="true">{~I:Refresh~}</span>
|
|
73
83
|
<span class="prsp-list-loading-label">Loading…</span>
|
|
74
84
|
</div>
|
|
75
|
-
|
|
85
|
+
<div class="prsp-list-skeleton" aria-hidden="true">
|
|
86
|
+
{~TS:PRSP-List-Skeleton-Row:Record.SkeletonRows~}
|
|
87
|
+
<div class="prsp-list-skeleton-fade"></div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
76
90
|
`
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
Hash: 'PRSP-List-Skeleton-Row',
|
|
94
|
+
Template: /*html*/`<div class="prsp-list-skeleton-row"></div>`
|
|
77
95
|
}
|
|
78
96
|
],
|
|
79
97
|
|
|
@@ -104,13 +122,20 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
104
122
|
recordList: null,
|
|
105
123
|
recordListHeader: null,
|
|
106
124
|
recordListEntry: null,
|
|
107
|
-
paginationBottom: null
|
|
125
|
+
paginationBottom: null,
|
|
126
|
+
columnChooser: null
|
|
108
127
|
};
|
|
109
128
|
|
|
110
129
|
// Identity (`RecordSet::FilterString::FilterExperience`) of the list currently painted into the DOM.
|
|
111
130
|
// When a route only changes the page (Offset/PageSize) and this still matches, we re-render just the
|
|
112
131
|
// rows + pagination instead of the whole view — see handleRecordSetListRoute / _paintRecordList.
|
|
113
132
|
this._renderedListIdentity = null;
|
|
133
|
+
|
|
134
|
+
// The last fully-composed list data (carrying the pristine ColumnCandidates) and the arguments of
|
|
135
|
+
// the last render call — together they let a column-visibility toggle repaint the rows from data
|
|
136
|
+
// already in hand (or, when a Lite fetch is missing the column, rerun the same render to refetch).
|
|
137
|
+
this._lastRecordListData = null;
|
|
138
|
+
this._lastListRenderArgs = null;
|
|
114
139
|
}
|
|
115
140
|
|
|
116
141
|
handleRecordSetListRoute(pRoutePayload)
|
|
@@ -192,10 +217,30 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
192
217
|
// When the list is already on screen, scope the spinner to just the rows area so the title,
|
|
193
218
|
// filters, and pagination stay put (and the expensive filter view isn't disturbed). On the first
|
|
194
219
|
// render the rows container doesn't exist yet, so fall back to the whole list destination.
|
|
195
|
-
const
|
|
220
|
+
const tmpRowsElements = this.pict.ContentAssignment.getElement('#PRSP_RecordList_Container');
|
|
221
|
+
const tmpRowsContainerPresent = tmpRowsElements.length > 0;
|
|
196
222
|
const tmpDestination = tmpRowsContainerPresent ? '#PRSP_RecordList_Container' : pRecordListData.RenderDestination;
|
|
223
|
+
// Fill roughly the visible viewport with ghost rows (each skeleton row occupies ~53px), capped to
|
|
224
|
+
// the preserved height so a short list doesn't over-fill, and clamped to a sane range.
|
|
225
|
+
const tmpViewportHeight = (typeof window !== 'undefined' && window.innerHeight) ? window.innerHeight : 800;
|
|
226
|
+
let tmpFillHeight = tmpViewportHeight + 200;
|
|
227
|
+
if (tmpRowsContainerPresent)
|
|
228
|
+
{
|
|
229
|
+
// Pin the rows area to its current height before swapping in the spinner, so the page doesn't
|
|
230
|
+
// collapse and yank the content below it — pagination, the page's footer/colored fill — up into
|
|
231
|
+
// the fold and back. The floor is released once the real rows render (see _paintRecordList).
|
|
232
|
+
const tmpCurrentHeight = tmpRowsElements[0].offsetHeight;
|
|
233
|
+
if (tmpCurrentHeight > 0)
|
|
234
|
+
{
|
|
235
|
+
tmpRowsElements[0].style.minHeight = `${ tmpCurrentHeight }px`;
|
|
236
|
+
tmpFillHeight = Math.min(tmpCurrentHeight, tmpFillHeight);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const tmpSkeletonRowCount = Math.max(6, Math.min(60, Math.ceil(tmpFillHeight / 53)));
|
|
197
240
|
this.pict.CSSMap.injectCSS();
|
|
198
|
-
|
|
241
|
+
// Render the spinner + enough skeleton ghost rows to fill the visible area (so no white void shows).
|
|
242
|
+
const tmpLoadingShellData = { SkeletonRows: new Array(tmpSkeletonRowCount).fill(0) };
|
|
243
|
+
this.pict.ContentAssignment.assignContent(tmpDestination, this.pict.parseTemplateByHash('PRSP-List-LoadingShell', tmpLoadingShellData));
|
|
199
244
|
}
|
|
200
245
|
catch (pError)
|
|
201
246
|
{
|
|
@@ -204,13 +249,19 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
204
249
|
}
|
|
205
250
|
}
|
|
206
251
|
|
|
207
|
-
|
|
252
|
+
/**
|
|
253
|
+
* The schema columns that never become list columns automatically: the entity's own
|
|
254
|
+
* identity pair plus the audit stamps. (Hosts that want one of these in the column
|
|
255
|
+
* chooser declare it as a curated column, optionally with DefaultHidden.)
|
|
256
|
+
*
|
|
257
|
+
* @param {string} pEntity - The entity name (for the ID/GUID identity columns)
|
|
258
|
+
* @return {Array<string>} The excluded column names.
|
|
259
|
+
*/
|
|
260
|
+
_getExcludedSchemaColumns(pEntity)
|
|
208
261
|
{
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
'ID' + tmpEntity,
|
|
213
|
-
'GUID' + tmpEntity,
|
|
262
|
+
return [
|
|
263
|
+
'ID' + pEntity,
|
|
264
|
+
'GUID' + pEntity,
|
|
214
265
|
'CreateDate',
|
|
215
266
|
'CreatingIDUser',
|
|
216
267
|
'DeleteDate',
|
|
@@ -219,6 +270,13 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
219
270
|
'UpdateDate',
|
|
220
271
|
'UpdatingIDUser',
|
|
221
272
|
];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
dynamicallyGenerateColumns(pRecordListData)
|
|
276
|
+
{
|
|
277
|
+
pRecordListData.TableCells = [];
|
|
278
|
+
const tmpEntity = pRecordListData.RecordSetConfiguration.Entity;
|
|
279
|
+
this.excludedByDefaultCells = this._getExcludedSchemaColumns(tmpEntity);
|
|
222
280
|
|
|
223
281
|
const tmpSchema = pRecordListData.RecordSchema;
|
|
224
282
|
const tmpProperties = tmpSchema?.properties;
|
|
@@ -243,6 +301,190 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
243
301
|
return pRecordListData;
|
|
244
302
|
}
|
|
245
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Map column name -> Meadow column Type from the entity schema, when available. The schema
|
|
306
|
+
* endpoint nests the canonical column array at MeadowSchema.MeadowSchema.Schema (with a
|
|
307
|
+
* legacy flat MeadowSchema.Schema fallback). Returns null when neither is present (e.g.
|
|
308
|
+
* non-Meadow providers) so callers can skip type-based exclusions.
|
|
309
|
+
*
|
|
310
|
+
* @param {Record<string, any>} pRecordSchema - The schema from getRecordSchema()
|
|
311
|
+
* @return {Record<string, string>|null} Column name -> Type map, or null.
|
|
312
|
+
*/
|
|
313
|
+
_getMeadowColumnTypes(pRecordSchema)
|
|
314
|
+
{
|
|
315
|
+
const tmpSchemaColumns = pRecordSchema?.MeadowSchema?.MeadowSchema?.Schema || pRecordSchema?.MeadowSchema?.Schema;
|
|
316
|
+
if (!Array.isArray(tmpSchemaColumns) || tmpSchemaColumns.length < 1)
|
|
317
|
+
{
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
/** @type {Record<string, string>} */
|
|
321
|
+
const tmpColumnTypes = {};
|
|
322
|
+
for (const tmpColumn of tmpSchemaColumns)
|
|
323
|
+
{
|
|
324
|
+
if (tmpColumn && tmpColumn.Column)
|
|
325
|
+
{
|
|
326
|
+
tmpColumnTypes[tmpColumn.Column] = tmpColumn.Type;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return tmpColumnTypes;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Whether a column candidate is effectively visible: an explicit user override wins,
|
|
334
|
+
* otherwise the candidate's default (visible unless DefaultHidden).
|
|
335
|
+
*
|
|
336
|
+
* @param {Record<string, any>} pCandidate - A ColumnCandidates entry
|
|
337
|
+
* @param {Record<string, boolean>} pOverrides - The per-recordset override map
|
|
338
|
+
* @return {boolean}
|
|
339
|
+
*/
|
|
340
|
+
_effectiveColumnVisibility(pCandidate, pOverrides)
|
|
341
|
+
{
|
|
342
|
+
if (pOverrides && (pCandidate.Key in pOverrides))
|
|
343
|
+
{
|
|
344
|
+
return (pOverrides[pCandidate.Key] === true);
|
|
345
|
+
}
|
|
346
|
+
return (pCandidate.DefaultHidden !== true);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Compute the visible TableCells for a paint from the pristine candidate list + the user's
|
|
351
|
+
* current overrides. Cells are per-paint shallow copies so host hooks can mutate them without
|
|
352
|
+
* bleeding into the candidates. An override set that hides everything falls back to the
|
|
353
|
+
* default-visible set (a fully empty table is a confusing dead end).
|
|
354
|
+
*
|
|
355
|
+
* @param {Array<Record<string, any>>} pCandidates - The pristine ColumnCandidates
|
|
356
|
+
* @param {string} pRecordSet - The record set (for the override lookup)
|
|
357
|
+
* @return {Array<Record<string, any>>} The visible cells, in candidate order.
|
|
358
|
+
*/
|
|
359
|
+
_computeVisibleTableCells(pCandidates, pRecordSet)
|
|
360
|
+
{
|
|
361
|
+
const tmpColumnProvider = this.pict.providers.ColumnDataProvider;
|
|
362
|
+
const tmpOverrides = tmpColumnProvider ? tmpColumnProvider.getColumnVisibilityOverrides(pRecordSet, 'List') : {};
|
|
363
|
+
let tmpVisibleCells = pCandidates
|
|
364
|
+
.filter((pCandidate) => this._effectiveColumnVisibility(pCandidate, tmpOverrides))
|
|
365
|
+
.map((pCell) => Object.assign({}, pCell));
|
|
366
|
+
if (tmpVisibleCells.length < 1)
|
|
367
|
+
{
|
|
368
|
+
this.log.warn(`RecordSetList: column visibility overrides for [${pRecordSet}] hid every column; rendering the default-visible set instead.`);
|
|
369
|
+
tmpVisibleCells = pCandidates
|
|
370
|
+
.filter((pCandidate) => pCandidate.DefaultHidden !== true)
|
|
371
|
+
.map((pCell) => Object.assign({}, pCell));
|
|
372
|
+
}
|
|
373
|
+
return tmpVisibleCells;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Compose the column-chooser candidate pool and reduce TableCells to the visible subset.
|
|
378
|
+
*
|
|
379
|
+
* No-op unless the recordset opts in with RecordSetListColumnChooser: true — the flag off
|
|
380
|
+
* leaves TableCells exactly as the existing paths computed it (including the manifest's
|
|
381
|
+
* shared array reference).
|
|
382
|
+
*
|
|
383
|
+
* Candidates are two tiers, in order:
|
|
384
|
+
* - Curated: the host-declared columns (manifest Descriptors or RecordSetListColumns),
|
|
385
|
+
* shallow-copied (the shared manifest TableCells entries are never mutated), default
|
|
386
|
+
* visible unless the column/descriptor declares DefaultHidden: true.
|
|
387
|
+
* - Schema: remaining scalar entity columns (identity/audit fields and blob Text/JSON
|
|
388
|
+
* columns excluded), default hidden, rendered via the generic ProcessCell template
|
|
389
|
+
* (entity-reference ID* columns resolve names exactly like dynamic columns do).
|
|
390
|
+
*
|
|
391
|
+
* The pristine candidates ride on pRecordListData.ColumnCandidates (module-owned — host
|
|
392
|
+
* hooks must not mutate it); TableCells becomes per-paint copies of the visible subset.
|
|
393
|
+
*
|
|
394
|
+
* @param {Record<string, any>} pRecordListData - The list data (TableCells already computed)
|
|
395
|
+
* @return {Record<string, any>} The same list data, candidates composed.
|
|
396
|
+
*/
|
|
397
|
+
_composeColumnCandidates(pRecordListData)
|
|
398
|
+
{
|
|
399
|
+
// Always an array (empty = render nothing): a missing address would make the chooser
|
|
400
|
+
// button's {~TS:~} iterate the record object's own keys instead of rendering nothing.
|
|
401
|
+
pRecordListData.ColumnChooserSlot = [];
|
|
402
|
+
const tmpConfig = pRecordListData.RecordSetConfiguration;
|
|
403
|
+
if (!tmpConfig || tmpConfig.RecordSetListColumnChooser !== true)
|
|
404
|
+
{
|
|
405
|
+
return pRecordListData;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Curated tier: shallow copies of whatever the host declared, flagged with source + default.
|
|
409
|
+
const tmpCandidates = (pRecordListData.TableCells || []).map((pCell) =>
|
|
410
|
+
Object.assign({}, pCell, { Source: 'Curated', DefaultHidden: (pCell.DefaultHidden === true) }));
|
|
411
|
+
const tmpCuratedKeys = {};
|
|
412
|
+
for (const tmpCell of tmpCandidates)
|
|
413
|
+
{
|
|
414
|
+
tmpCuratedKeys[tmpCell.Key] = true;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Schema tier: every remaining scalar entity column, default hidden.
|
|
418
|
+
const tmpExcludedColumns = this._getExcludedSchemaColumns(tmpConfig.Entity);
|
|
419
|
+
const tmpProperties = pRecordListData.RecordSchema?.properties;
|
|
420
|
+
const tmpMeadowColumnTypes = this._getMeadowColumnTypes(pRecordListData.RecordSchema);
|
|
421
|
+
const tmpBlobTypes = { 'Text': true, 'JSON': true };
|
|
422
|
+
const tmpSchemaCandidates = [];
|
|
423
|
+
for (const tmpColumn in tmpProperties)
|
|
424
|
+
{
|
|
425
|
+
if (!tmpProperties.hasOwnProperty(tmpColumn) || tmpCuratedKeys[tmpColumn] || tmpExcludedColumns.includes(tmpColumn))
|
|
426
|
+
{
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
// When the Meadow schema is available, only offer real non-blob columns — a JSON-schema-only
|
|
430
|
+
// property is not fetchable by a Lite projection, and blob columns are why Lite exists.
|
|
431
|
+
if (tmpMeadowColumnTypes && (!(tmpColumn in tmpMeadowColumnTypes) || tmpBlobTypes[tmpMeadowColumnTypes[tmpColumn]]))
|
|
432
|
+
{
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
tmpSchemaCandidates.push(
|
|
436
|
+
{
|
|
437
|
+
Key: tmpColumn,
|
|
438
|
+
DisplayName: tmpProperties[tmpColumn].title || tmpColumn,
|
|
439
|
+
ManifestHash: 'Default',
|
|
440
|
+
PictDashboard: { ValueTemplate: '{~ProcessCell:Record.Data.Key~}' },
|
|
441
|
+
Source: 'Schema',
|
|
442
|
+
DefaultHidden: true,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
tmpSchemaCandidates.sort((pA, pB) => String(pA.DisplayName).localeCompare(String(pB.DisplayName)));
|
|
446
|
+
|
|
447
|
+
// Audit tier: the identity pair + audit stamps, with friendly labels, trailing the schema
|
|
448
|
+
// tier. Default hidden; the create/update/delete user references resolve names via the same
|
|
449
|
+
// ProcessCell path as any other entity-reference column. (Pair with the show-deleted filter
|
|
450
|
+
// — RecordSetListShowDeletedFilter — to make the three Deleted* columns meaningful.)
|
|
451
|
+
const tmpAuditColumnLabels =
|
|
452
|
+
{
|
|
453
|
+
[`ID${tmpConfig.Entity}`]: 'ID',
|
|
454
|
+
[`GUID${tmpConfig.Entity}`]: 'GUID',
|
|
455
|
+
'CreateDate': 'Created',
|
|
456
|
+
'CreatingIDUser': 'Created by',
|
|
457
|
+
'UpdateDate': 'Updated',
|
|
458
|
+
'UpdatingIDUser': 'Updated by',
|
|
459
|
+
'Deleted': 'Deleted',
|
|
460
|
+
'DeleteDate': 'Deleted on',
|
|
461
|
+
'DeletingIDUser': 'Deleted by',
|
|
462
|
+
};
|
|
463
|
+
const tmpAuditCandidates = [];
|
|
464
|
+
for (const tmpColumn of Object.keys(tmpAuditColumnLabels))
|
|
465
|
+
{
|
|
466
|
+
if (!tmpProperties || !tmpProperties.hasOwnProperty(tmpColumn) || tmpCuratedKeys[tmpColumn])
|
|
467
|
+
{
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
tmpAuditCandidates.push(
|
|
471
|
+
{
|
|
472
|
+
Key: tmpColumn,
|
|
473
|
+
DisplayName: tmpAuditColumnLabels[tmpColumn],
|
|
474
|
+
ManifestHash: 'Default',
|
|
475
|
+
PictDashboard: { ValueTemplate: '{~ProcessCell:Record.Data.Key~}' },
|
|
476
|
+
Source: 'Audit',
|
|
477
|
+
DefaultHidden: true,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
pRecordListData.ColumnCandidates = tmpCandidates.concat(tmpSchemaCandidates, tmpAuditCandidates);
|
|
482
|
+
pRecordListData.TableCells = this._computeVisibleTableCells(pRecordListData.ColumnCandidates, pRecordListData.RecordSet);
|
|
483
|
+
// One-or-zero-element array driving the chooser button render (the {~TS:~} conditional trick).
|
|
484
|
+
pRecordListData.ColumnChooserSlot = [ { RecordSet: pRecordListData.RecordSet } ];
|
|
485
|
+
return pRecordListData;
|
|
486
|
+
}
|
|
487
|
+
|
|
246
488
|
/**
|
|
247
489
|
* @param {Record<string, any>} pRecordSetConfiguration
|
|
248
490
|
* @param {string} pProviderHash
|
|
@@ -263,6 +505,10 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
263
505
|
return;
|
|
264
506
|
}
|
|
265
507
|
|
|
508
|
+
// Remember how this list was rendered so a column-visibility change can rerun the exact same
|
|
509
|
+
// render (the manifest delegation below overwrites this with its own, which is what we want).
|
|
510
|
+
this._lastListRenderArgs = { Method: 'renderList', Args: [ pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize ] };
|
|
511
|
+
|
|
266
512
|
if (pRecordSetConfiguration.RecordSetListManifestOnly)
|
|
267
513
|
{
|
|
268
514
|
const tmpManifestHash = pRecordSetConfiguration.RecordSetListDefaultManifest || pRecordSetConfiguration.RecordSetListManifests?.[0];
|
|
@@ -490,6 +736,8 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
490
736
|
{
|
|
491
737
|
this.dynamicallyGenerateColumns(tmpRecordListData);
|
|
492
738
|
}
|
|
739
|
+
this._composeColumnCandidates(tmpRecordListData);
|
|
740
|
+
this._lastRecordListData = tmpRecordListData;
|
|
493
741
|
tmpRecordListData = this.onBeforeRenderList(tmpRecordListData);
|
|
494
742
|
|
|
495
743
|
this._paintRecordList(tmpRecordListData, pBodyOnly);
|
|
@@ -521,6 +769,11 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
521
769
|
return;
|
|
522
770
|
}
|
|
523
771
|
|
|
772
|
+
// Remember how this list was rendered so a column-visibility change can rerun the exact same
|
|
773
|
+
// render. When renderList delegated here this overwrite wins — a rerun skips re-resolving the
|
|
774
|
+
// manifest — and hosts that call renderListFromManifest directly are covered the same way.
|
|
775
|
+
this._lastListRenderArgs = { Method: 'renderListFromManifest', Args: [ pManifest, pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize ] };
|
|
776
|
+
|
|
524
777
|
let tmpTitle = pRecordSetConfiguration.Title || pRecordSetConfiguration.RecordSet;
|
|
525
778
|
if (pManifest && pManifest.TitleTemplate)
|
|
526
779
|
{
|
|
@@ -738,6 +991,8 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
738
991
|
}
|
|
739
992
|
}
|
|
740
993
|
|
|
994
|
+
this._composeColumnCandidates(tmpRecordListData);
|
|
995
|
+
this._lastRecordListData = tmpRecordListData;
|
|
741
996
|
tmpRecordListData = this.onBeforeRenderList(tmpRecordListData);
|
|
742
997
|
|
|
743
998
|
this.pict.providers.DynamicRecordsetSolver.solveDashboard(pManifest, tmpRecordListData.Records.Records);
|
|
@@ -789,7 +1044,148 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
789
1044
|
// rows, each into its own stable container. The filter view, title, and header list are left as-is.
|
|
790
1045
|
this.childViews.paginationTop.renderAsync('PRSP_Renderable_PaginationTop', '#PRSP_PaginationTop_Container', pRecordListData, null, () => { });
|
|
791
1046
|
this.childViews.paginationBottom.renderAsync('PRSP_Renderable_PaginationBottom', '#PRSP_PaginationBottom_Container', pRecordListData, null, () => { });
|
|
792
|
-
this.childViews.recordList.renderAsync('PRSP_Renderable_RecordList', '#PRSP_RecordList_Container', pRecordListData, null,
|
|
1047
|
+
this.childViews.recordList.renderAsync('PRSP_Renderable_RecordList', '#PRSP_RecordList_Container', pRecordListData, null,
|
|
1048
|
+
function (pError)
|
|
1049
|
+
{
|
|
1050
|
+
// Release the height floor that was pinned while the spinner showed (see _projectLoadingShell),
|
|
1051
|
+
// now that the real rows are back in and the container can size to its content again.
|
|
1052
|
+
const tmpRows = this.pict.ContentAssignment.getElement('#PRSP_RecordList_Container');
|
|
1053
|
+
if (tmpRows.length)
|
|
1054
|
+
{
|
|
1055
|
+
tmpRows[0].style.minHeight = '';
|
|
1056
|
+
}
|
|
1057
|
+
fLogRendered(pError);
|
|
1058
|
+
}.bind(this));
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Set a column's visibility for the currently rendered list (called by the column chooser).
|
|
1063
|
+
*
|
|
1064
|
+
* Persists the override, then repaints the rows + pagination body-only from the data already
|
|
1065
|
+
* in hand — except when a Lite-fetched list is showing a schema-tier column whose values were
|
|
1066
|
+
* never fetched, in which case the same render is rerun so the provider widens the projection.
|
|
1067
|
+
*
|
|
1068
|
+
* @param {string} pRecordSet - The record set the column belongs to (stale-chooser guard)
|
|
1069
|
+
* @param {string} pKey - The column key
|
|
1070
|
+
* @param {boolean} pVisible - Whether the column should be visible
|
|
1071
|
+
* @return {boolean} The column's resulting visibility.
|
|
1072
|
+
*/
|
|
1073
|
+
setColumnVisibility(pRecordSet, pKey, pVisible)
|
|
1074
|
+
{
|
|
1075
|
+
const tmpListData = this._lastRecordListData;
|
|
1076
|
+
if (!tmpListData || tmpListData.RecordSet !== pRecordSet || !Array.isArray(tmpListData.ColumnCandidates))
|
|
1077
|
+
{
|
|
1078
|
+
this.log.warn(`RecordSetList: setColumnVisibility for [${pRecordSet}.${pKey}] ignored — that list is not the one currently rendered.`);
|
|
1079
|
+
return false;
|
|
1080
|
+
}
|
|
1081
|
+
const tmpCandidate = tmpListData.ColumnCandidates.find((pCandidate) => pCandidate.Key === pKey);
|
|
1082
|
+
if (!tmpCandidate)
|
|
1083
|
+
{
|
|
1084
|
+
this.log.warn(`RecordSetList: setColumnVisibility for unknown column [${pKey}] on [${pRecordSet}] ignored.`);
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
const tmpColumnProvider = this.pict.providers.ColumnDataProvider;
|
|
1088
|
+
if (!tmpColumnProvider)
|
|
1089
|
+
{
|
|
1090
|
+
return this._effectiveColumnVisibility(tmpCandidate, {});
|
|
1091
|
+
}
|
|
1092
|
+
// Keep at least one column visible — an all-hidden table is a confusing dead end.
|
|
1093
|
+
if (pVisible !== true)
|
|
1094
|
+
{
|
|
1095
|
+
const tmpOverrides = tmpColumnProvider.getColumnVisibilityOverrides(pRecordSet, 'List');
|
|
1096
|
+
const tmpVisibleCount = tmpListData.ColumnCandidates.filter((pCandidate) => this._effectiveColumnVisibility(pCandidate, tmpOverrides)).length;
|
|
1097
|
+
if ((tmpVisibleCount <= 1) && this._effectiveColumnVisibility(tmpCandidate, tmpOverrides))
|
|
1098
|
+
{
|
|
1099
|
+
this.log.warn(`RecordSetList: refusing to hide the last visible column [${pKey}] on [${pRecordSet}].`);
|
|
1100
|
+
return true;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
tmpColumnProvider.setColumnVisibilityOverride(pRecordSet, 'List', pKey, (pVisible === true));
|
|
1104
|
+
|
|
1105
|
+
// Lite projections omit unrequested columns entirely, so a newly shown schema- or audit-tier
|
|
1106
|
+
// column with no data in the fetched records needs one refetch (the provider reads the override
|
|
1107
|
+
// map at fetch time and widens the projection). Curated/manifest columns are always fetched or
|
|
1108
|
+
// solved, and schema/audit keys are flat — a first-record key check is sound. (The identity pair,
|
|
1109
|
+
// CreatingIDUser, and UpdateDate ride free in every Lite record, so they pass the key check.)
|
|
1110
|
+
const tmpRecords = (tmpListData.Records && Array.isArray(tmpListData.Records.Records)) ? tmpListData.Records.Records : [];
|
|
1111
|
+
const tmpNeedsRefetch = (pVisible === true)
|
|
1112
|
+
&& (tmpCandidate.Source !== 'Curated')
|
|
1113
|
+
&& (tmpListData.RecordSetConfiguration && tmpListData.RecordSetConfiguration.RecordSetListLiteFetch === true)
|
|
1114
|
+
&& (tmpRecords.length > 0)
|
|
1115
|
+
&& !(pKey in tmpRecords[0]);
|
|
1116
|
+
if (tmpNeedsRefetch)
|
|
1117
|
+
{
|
|
1118
|
+
this._rerunLastListRender();
|
|
1119
|
+
}
|
|
1120
|
+
else
|
|
1121
|
+
{
|
|
1122
|
+
this._repaintWithColumnState();
|
|
1123
|
+
}
|
|
1124
|
+
return (pVisible === true);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Clear every column-visibility override for the currently rendered list and repaint with the
|
|
1129
|
+
* defaults (called by the column chooser's Reset). Never needs a refetch: resetting only
|
|
1130
|
+
* restores curated columns (always fetched) and hides schema extras.
|
|
1131
|
+
*
|
|
1132
|
+
* @param {string} pRecordSet - The record set to reset (stale-chooser guard)
|
|
1133
|
+
* @return {boolean} True when the reset happened.
|
|
1134
|
+
*/
|
|
1135
|
+
resetColumnVisibility(pRecordSet)
|
|
1136
|
+
{
|
|
1137
|
+
const tmpListData = this._lastRecordListData;
|
|
1138
|
+
if (!tmpListData || tmpListData.RecordSet !== pRecordSet)
|
|
1139
|
+
{
|
|
1140
|
+
this.log.warn(`RecordSetList: resetColumnVisibility for [${pRecordSet}] ignored — that list is not the one currently rendered.`);
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
const tmpColumnProvider = this.pict.providers.ColumnDataProvider;
|
|
1144
|
+
if (tmpColumnProvider)
|
|
1145
|
+
{
|
|
1146
|
+
tmpColumnProvider.clearColumnVisibilityOverrides(pRecordSet, 'List');
|
|
1147
|
+
}
|
|
1148
|
+
this._repaintWithColumnState();
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Repaint the rows + pagination (body-only) from the last composed list data, with TableCells
|
|
1154
|
+
* recomputed from the pristine candidates + current overrides. onBeforeRenderList is re-invoked
|
|
1155
|
+
* — it is the documented seam where hosts append custom cells, and rebuilding TableCells from
|
|
1156
|
+
* candidates each paint means hook mutations apply exactly once per paint. (Hosts that decorate
|
|
1157
|
+
* Records in the hook must keep that decoration idempotent; the hook already re-runs on every
|
|
1158
|
+
* page change.) No loading shell: the data is already in hand, so the swap is immediate.
|
|
1159
|
+
*
|
|
1160
|
+
* @return {void}
|
|
1161
|
+
*/
|
|
1162
|
+
_repaintWithColumnState()
|
|
1163
|
+
{
|
|
1164
|
+
const tmpSourceData = this._lastRecordListData;
|
|
1165
|
+
if (!tmpSourceData || !Array.isArray(tmpSourceData.ColumnCandidates))
|
|
1166
|
+
{
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
let tmpPaintData = Object.assign({}, tmpSourceData);
|
|
1170
|
+
tmpPaintData.TableCells = this._computeVisibleTableCells(tmpSourceData.ColumnCandidates, tmpSourceData.RecordSet);
|
|
1171
|
+
tmpPaintData = this.onBeforeRenderList(tmpPaintData);
|
|
1172
|
+
this._paintRecordList(tmpPaintData, true);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* Rerun the last list render with the same arguments (body-only — the list shell and filters
|
|
1177
|
+
* are already on screen). Used when a column toggle needs a refetch under Lite.
|
|
1178
|
+
*
|
|
1179
|
+
* @return {Promise<void>|undefined}
|
|
1180
|
+
*/
|
|
1181
|
+
_rerunLastListRender()
|
|
1182
|
+
{
|
|
1183
|
+
if (!this._lastListRenderArgs)
|
|
1184
|
+
{
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const tmpArgs = this._lastListRenderArgs.Args.concat([ true ]);
|
|
1188
|
+
return this[this._lastListRenderArgs.Method](...tmpArgs);
|
|
793
1189
|
}
|
|
794
1190
|
|
|
795
1191
|
onInitialize()
|
|
@@ -802,6 +1198,7 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
802
1198
|
this.childViews.recordListHeader = this.pict.addView('PRSP-List-RecordListHeader', viewRecordListHeader.default_configuration, viewRecordListHeader);
|
|
803
1199
|
this.childViews.recordListEntry = this.pict.addView('PRSP-List-RecordListEntry', viewRecordListEntry.default_configuration, viewRecordListEntry);
|
|
804
1200
|
this.childViews.paginationBottom = this.pict.addView('PRSP-List-PaginationBottom', viewPaginationBottom.default_configuration, viewPaginationBottom);
|
|
1201
|
+
this.childViews.columnChooser = this.pict.addView('PRSP-List-ColumnChooser', viewColumnChooser.default_configuration, viewColumnChooser);
|
|
805
1202
|
|
|
806
1203
|
// Initialize the subviews
|
|
807
1204
|
this.childViews.headerList.initialize();
|
|
@@ -811,6 +1208,7 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
811
1208
|
this.childViews.recordListHeader.initialize();
|
|
812
1209
|
this.childViews.recordListEntry.initialize();
|
|
813
1210
|
this.childViews.paginationBottom.initialize();
|
|
1211
|
+
this.childViews.columnChooser.initialize();
|
|
814
1212
|
|
|
815
1213
|
return super.onInitialize();
|
|
816
1214
|
}
|