pict-section-recordset 1.9.6 → 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 +390 -15
- 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)
|
|
@@ -195,19 +220,27 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
195
220
|
const tmpRowsElements = this.pict.ContentAssignment.getElement('#PRSP_RecordList_Container');
|
|
196
221
|
const tmpRowsContainerPresent = tmpRowsElements.length > 0;
|
|
197
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;
|
|
198
227
|
if (tmpRowsContainerPresent)
|
|
199
228
|
{
|
|
200
|
-
// Pin the rows area to its current height before swapping in the
|
|
201
|
-
//
|
|
202
|
-
//
|
|
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).
|
|
203
232
|
const tmpCurrentHeight = tmpRowsElements[0].offsetHeight;
|
|
204
233
|
if (tmpCurrentHeight > 0)
|
|
205
234
|
{
|
|
206
235
|
tmpRowsElements[0].style.minHeight = `${ tmpCurrentHeight }px`;
|
|
236
|
+
tmpFillHeight = Math.min(tmpCurrentHeight, tmpFillHeight);
|
|
207
237
|
}
|
|
208
238
|
}
|
|
239
|
+
const tmpSkeletonRowCount = Math.max(6, Math.min(60, Math.ceil(tmpFillHeight / 53)));
|
|
209
240
|
this.pict.CSSMap.injectCSS();
|
|
210
|
-
|
|
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));
|
|
211
244
|
}
|
|
212
245
|
catch (pError)
|
|
213
246
|
{
|
|
@@ -216,13 +249,19 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
216
249
|
}
|
|
217
250
|
}
|
|
218
251
|
|
|
219
|
-
|
|
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)
|
|
220
261
|
{
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
'ID' + tmpEntity,
|
|
225
|
-
'GUID' + tmpEntity,
|
|
262
|
+
return [
|
|
263
|
+
'ID' + pEntity,
|
|
264
|
+
'GUID' + pEntity,
|
|
226
265
|
'CreateDate',
|
|
227
266
|
'CreatingIDUser',
|
|
228
267
|
'DeleteDate',
|
|
@@ -231,6 +270,13 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
231
270
|
'UpdateDate',
|
|
232
271
|
'UpdatingIDUser',
|
|
233
272
|
];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
dynamicallyGenerateColumns(pRecordListData)
|
|
276
|
+
{
|
|
277
|
+
pRecordListData.TableCells = [];
|
|
278
|
+
const tmpEntity = pRecordListData.RecordSetConfiguration.Entity;
|
|
279
|
+
this.excludedByDefaultCells = this._getExcludedSchemaColumns(tmpEntity);
|
|
234
280
|
|
|
235
281
|
const tmpSchema = pRecordListData.RecordSchema;
|
|
236
282
|
const tmpProperties = tmpSchema?.properties;
|
|
@@ -255,6 +301,190 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
255
301
|
return pRecordListData;
|
|
256
302
|
}
|
|
257
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
|
+
|
|
258
488
|
/**
|
|
259
489
|
* @param {Record<string, any>} pRecordSetConfiguration
|
|
260
490
|
* @param {string} pProviderHash
|
|
@@ -275,6 +505,10 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
275
505
|
return;
|
|
276
506
|
}
|
|
277
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
|
+
|
|
278
512
|
if (pRecordSetConfiguration.RecordSetListManifestOnly)
|
|
279
513
|
{
|
|
280
514
|
const tmpManifestHash = pRecordSetConfiguration.RecordSetListDefaultManifest || pRecordSetConfiguration.RecordSetListManifests?.[0];
|
|
@@ -502,6 +736,8 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
502
736
|
{
|
|
503
737
|
this.dynamicallyGenerateColumns(tmpRecordListData);
|
|
504
738
|
}
|
|
739
|
+
this._composeColumnCandidates(tmpRecordListData);
|
|
740
|
+
this._lastRecordListData = tmpRecordListData;
|
|
505
741
|
tmpRecordListData = this.onBeforeRenderList(tmpRecordListData);
|
|
506
742
|
|
|
507
743
|
this._paintRecordList(tmpRecordListData, pBodyOnly);
|
|
@@ -533,6 +769,11 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
533
769
|
return;
|
|
534
770
|
}
|
|
535
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
|
+
|
|
536
777
|
let tmpTitle = pRecordSetConfiguration.Title || pRecordSetConfiguration.RecordSet;
|
|
537
778
|
if (pManifest && pManifest.TitleTemplate)
|
|
538
779
|
{
|
|
@@ -750,6 +991,8 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
750
991
|
}
|
|
751
992
|
}
|
|
752
993
|
|
|
994
|
+
this._composeColumnCandidates(tmpRecordListData);
|
|
995
|
+
this._lastRecordListData = tmpRecordListData;
|
|
753
996
|
tmpRecordListData = this.onBeforeRenderList(tmpRecordListData);
|
|
754
997
|
|
|
755
998
|
this.pict.providers.DynamicRecordsetSolver.solveDashboard(pManifest, tmpRecordListData.Records.Records);
|
|
@@ -815,6 +1058,136 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
815
1058
|
}.bind(this));
|
|
816
1059
|
}
|
|
817
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);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
818
1191
|
onInitialize()
|
|
819
1192
|
{
|
|
820
1193
|
this.childViews.headerList = this.pict.addView('PRSP-List-HeaderList', viewHeaderList.default_configuration, viewHeaderList);
|
|
@@ -825,6 +1198,7 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
825
1198
|
this.childViews.recordListHeader = this.pict.addView('PRSP-List-RecordListHeader', viewRecordListHeader.default_configuration, viewRecordListHeader);
|
|
826
1199
|
this.childViews.recordListEntry = this.pict.addView('PRSP-List-RecordListEntry', viewRecordListEntry.default_configuration, viewRecordListEntry);
|
|
827
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);
|
|
828
1202
|
|
|
829
1203
|
// Initialize the subviews
|
|
830
1204
|
this.childViews.headerList.initialize();
|
|
@@ -834,6 +1208,7 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
834
1208
|
this.childViews.recordListHeader.initialize();
|
|
835
1209
|
this.childViews.recordListEntry.initialize();
|
|
836
1210
|
this.childViews.paginationBottom.initialize();
|
|
1211
|
+
this.childViews.columnChooser.initialize();
|
|
837
1212
|
|
|
838
1213
|
return super.onInitialize();
|
|
839
1214
|
}
|
|
@@ -29,7 +29,12 @@ const _DEFAULT_CONFIGURATION__Read = (
|
|
|
29
29
|
AutoSolveOrdinal: 0,
|
|
30
30
|
|
|
31
31
|
CSS: /*css*/`
|
|
32
|
-
|
|
32
|
+
/* Soft-deleted record banner (the ViewDeleted route) — quiet alarm, theme-token driven. */
|
|
33
|
+
.prsp-read-deleted-banner { display: flex; align-items: center; gap: 0.5rem; margin: 0 0 0.85rem; padding: 0.55rem 0.85rem;
|
|
34
|
+
border: 1px solid var(--theme-color-status-error, #c0504d); border-radius: 8px; font-size: 0.92rem;
|
|
35
|
+
color: var(--theme-color-status-error, #c0504d);
|
|
36
|
+
background: color-mix(in srgb, var(--theme-color-status-error, #c0504d) 8%, transparent); }
|
|
37
|
+
.prsp-audit-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin: 0 0 1rem; }
|
|
33
38
|
.prsp-audit-line { display: inline-flex; align-items: center; gap: 0.4rem; color: var(--theme-color-text-muted, #6b7686); font-size: 0.85rem; }
|
|
34
39
|
.prsp-audit-line .pict-icon { font-size: 0.8rem; }
|
|
35
40
|
.prsp-audit-line strong { color: var(--theme-color-text-secondary, #45505f); font-weight: 600; }
|
|
@@ -256,12 +261,15 @@ const _DEFAULT_CONFIGURATION__Read = (
|
|
|
256
261
|
{
|
|
257
262
|
Hash: 'PRSP-Read-Link-URL-Template',
|
|
258
263
|
// TODO: Double payload pattern...
|
|
259
|
-
|
|
264
|
+
// Soft-deleted rows (visible via the show-deleted filter) route to ViewDeleted, whose
|
|
265
|
+
// lookup explicitly includes deleted records — a plain View would find nothing.
|
|
266
|
+
Template: `#/PSRS/{~D:Record.Payload.Payload.RecordSet~}/View{~NE:Record.Payload.Data.Deleted^Deleted~}/{~DVBK:Record.Payload.Data:Record.Payload.Payload.GUIDAddress~}`
|
|
260
267
|
},
|
|
261
268
|
// --- Record audit header (themeable; apps brand via --theme-color-* tokens) ---
|
|
262
269
|
{
|
|
263
270
|
Hash: 'PRSP-Read-RecordAuditHeader-Template',
|
|
264
271
|
Template: /*html*/`
|
|
272
|
+
{~TS:PRSP-Read-DeletedBanner-Template:AppData.PRSP_RecordAudit.DeletedBannerSlot~}
|
|
265
273
|
<div class="prsp-audit-header">
|
|
266
274
|
<div class="prsp-audit-activity">{~TS:PRSP-Read-RecordAudit-Line-Template:AppData.PRSP_RecordAudit.ActivitySlot~}</div>
|
|
267
275
|
<div class="prsp-audit-anchor">
|
|
@@ -299,6 +307,16 @@ const _DEFAULT_CONFIGURATION__Read = (
|
|
|
299
307
|
Hash: 'PRSP-Read-RecordAudit-Deleted-Template',
|
|
300
308
|
Template: /*html*/`<dt>Deleted</dt><dd class="is-deleted">{~DateFormat:Record.Date^MMM D, YYYY - h:mm A~} <small>by {~E:User^Record.UserID^PRSP-Read-RecordAudit-UserName-Template~}</small></dd>`
|
|
301
309
|
},
|
|
310
|
+
{
|
|
311
|
+
// Soft-deleted record banner (the ViewDeleted route, or a record deleted out from
|
|
312
|
+
// under a normal View). The one-or-zero-element DeletedBannerSlot drives it.
|
|
313
|
+
Hash: 'PRSP-Read-DeletedBanner-Template',
|
|
314
|
+
Template: /*html*/`<div class="prsp-read-deleted-banner">{~I:Warning~} This record has been deleted{~TIfAbs:PRSP-Read-DeletedBanner-Date-Template:Record:Record.HasDate^TRUE^~}.</div>`
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
Hash: 'PRSP-Read-DeletedBanner-Date-Template',
|
|
318
|
+
Template: /*html*/` on {~DateFormat:Record.Date^MMM D, YYYY - h:mm A~}`
|
|
319
|
+
},
|
|
302
320
|
],
|
|
303
321
|
|
|
304
322
|
Renderables:
|
|
@@ -404,8 +422,35 @@ class viewRecordSetRead extends libPictRecordSetRecordView
|
|
|
404
422
|
}
|
|
405
423
|
|
|
406
424
|
this.action = 'View';
|
|
425
|
+
this.viewingDeletedRecord = false;
|
|
426
|
+
const tmpProviderConfiguration = this.pict.PictSectionRecordSet.recordSetProviderConfigurations[pRoutePayload.data.RecordSet];
|
|
427
|
+
this.layoutType = tmpProviderConfiguration?.ReadLayout || 'Basic';
|
|
428
|
+
const tmpProviderHash = `RSP-Provider-${pRoutePayload.data.RecordSet}`;
|
|
429
|
+
|
|
430
|
+
tmpProviderConfiguration.RoutePayload = pRoutePayload;
|
|
431
|
+
tmpProviderConfiguration.RecordSet = pRoutePayload.data.RecordSet;
|
|
432
|
+
tmpProviderConfiguration.GUIDRecord = pRoutePayload.data.GUIDRecord;
|
|
433
|
+
|
|
434
|
+
return this.renderRead(tmpProviderConfiguration, tmpProviderHash, pRoutePayload.data.GUIDRecord);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* The deleted-record read route (`/PSRS/:RecordSet/ViewDeleted/:GUIDRecord`): identical to the
|
|
439
|
+
* View route except the record lookup explicitly includes soft-deleted rows (a normal View of a
|
|
440
|
+
* deleted record finds nothing — delete tracking filters it out — and renders broken). The flag
|
|
441
|
+
* also rides the read data as ViewingDeletedRecord, which drives the deleted banner.
|
|
442
|
+
*/
|
|
443
|
+
handleRecordSetReadDeletedRoute(pRoutePayload)
|
|
444
|
+
{
|
|
445
|
+
if (typeof(pRoutePayload) != 'object')
|
|
446
|
+
{
|
|
447
|
+
throw new Error(`Pict RecordSet Read view route handler called with invalid route payload.`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
this.action = 'View';
|
|
451
|
+
this.viewingDeletedRecord = true;
|
|
407
452
|
const tmpProviderConfiguration = this.pict.PictSectionRecordSet.recordSetProviderConfigurations[pRoutePayload.data.RecordSet];
|
|
408
|
-
this.layoutType = tmpProviderConfiguration?.ReadLayout || 'Basic';
|
|
453
|
+
this.layoutType = tmpProviderConfiguration?.ReadLayout || 'Basic';
|
|
409
454
|
const tmpProviderHash = `RSP-Provider-${pRoutePayload.data.RecordSet}`;
|
|
410
455
|
|
|
411
456
|
tmpProviderConfiguration.RoutePayload = pRoutePayload;
|
|
@@ -423,8 +468,9 @@ class viewRecordSetRead extends libPictRecordSetRecordView
|
|
|
423
468
|
}
|
|
424
469
|
|
|
425
470
|
this.action = 'Edit';
|
|
471
|
+
this.viewingDeletedRecord = false;
|
|
426
472
|
const tmpProviderConfiguration = this.pict.PictSectionRecordSet.recordSetProviderConfigurations[pRoutePayload.data.RecordSet];
|
|
427
|
-
this.layoutType = tmpProviderConfiguration?.ReadLayout || 'Basic';
|
|
473
|
+
this.layoutType = tmpProviderConfiguration?.ReadLayout || 'Basic';
|
|
428
474
|
const tmpProviderHash = `RSP-Provider-${pRoutePayload.data.RecordSet}`;
|
|
429
475
|
|
|
430
476
|
tmpProviderConfiguration.RoutePayload = pRoutePayload;
|
|
@@ -437,6 +483,7 @@ class viewRecordSetRead extends libPictRecordSetRecordView
|
|
|
437
483
|
addRoutes(pPictRouter)
|
|
438
484
|
{
|
|
439
485
|
pPictRouter.addRoute('/PSRS/:RecordSet/View/:GUIDRecord', this.handleRecordSetReadRoute.bind(this));
|
|
486
|
+
pPictRouter.addRoute('/PSRS/:RecordSet/ViewDeleted/:GUIDRecord', this.handleRecordSetReadDeletedRoute.bind(this));
|
|
440
487
|
pPictRouter.addRoute('/PSRS/:RecordSet/Edit/:GUIDRecord', this.handleRecordSetEditRoute.bind(this));
|
|
441
488
|
return true;
|
|
442
489
|
}
|
|
@@ -507,7 +554,10 @@ class viewRecordSetRead extends libPictRecordSetRecordView
|
|
|
507
554
|
ActivitySlot: tmpActivitySlot,
|
|
508
555
|
CreatedSlot: tmpHasCreate ? [{ Date: pRecord.CreateDate, UserID: pRecord.CreatingIDUser }] : [],
|
|
509
556
|
UpdatedSlot: tmpHasUpdate ? [{ Date: pRecord.UpdateDate, UserID: pRecord.UpdatingIDUser }] : [],
|
|
510
|
-
DeletedSlot: tmpDeleted ? [{ Date: pRecord.DeleteDate, UserID: pRecord.DeletingIDUser }] : []
|
|
557
|
+
DeletedSlot: tmpDeleted ? [{ Date: pRecord.DeleteDate, UserID: pRecord.DeletingIDUser }] : [],
|
|
558
|
+
// The ViewDeleted route's banner: present whenever the record is soft-deleted (whether the
|
|
559
|
+
// user arrived via ViewDeleted or the record was deleted out from under a normal View).
|
|
560
|
+
DeletedBannerSlot: (!!pRecord.Deleted) ? [{ Date: pRecord.DeleteDate, UserID: pRecord.DeletingIDUser, HasDate: this._validAuditDate(pRecord.DeleteDate) }] : []
|
|
511
561
|
};
|
|
512
562
|
}
|
|
513
563
|
|
|
@@ -753,8 +803,9 @@ class viewRecordSetRead extends libPictRecordSetRecordView
|
|
|
753
803
|
// TODO: This should be coming from the schema but that can come after we discuss how we deal with default routing
|
|
754
804
|
tmpRecordReadData.GUIDAddress = this.pict.providers[pProviderHash].getGUIDField();
|
|
755
805
|
|
|
756
|
-
tmpRecordReadData.Record = await this.pict.providers[pProviderHash].getRecordByGUID(pRecordGUID);
|
|
806
|
+
tmpRecordReadData.Record = await this.pict.providers[pProviderHash].getRecordByGUID(pRecordGUID, this.viewingDeletedRecord === true);
|
|
757
807
|
tmpRecordReadData.RecordSchema = await this.pict.providers[pProviderHash].getRecordSchema();
|
|
808
|
+
tmpRecordReadData.ViewingDeletedRecord = (this.viewingDeletedRecord === true);
|
|
758
809
|
this.pict.AppData[`${ tmpRecordReadData.RecordSet }Details`] = tmpRecordReadData.Record;
|
|
759
810
|
|
|
760
811
|
// Build the audit header state (first-class activity line + the Details modal) for this record.
|
|
@@ -847,6 +898,14 @@ class viewRecordSetRead extends libPictRecordSetRecordView
|
|
|
847
898
|
document.getElementById('PRSP-Read-SaveButton').classList.remove('record-button-bar-hidden');
|
|
848
899
|
document.getElementById('PRSP-Read-CancelButton').classList.remove('record-button-bar-hidden');
|
|
849
900
|
}
|
|
901
|
+
else if (this.viewingDeletedRecord === true)
|
|
902
|
+
{
|
|
903
|
+
// A deleted record can't be edited (the Edit route's lookup excludes deleted rows) —
|
|
904
|
+
// the ViewDeleted page is read-only.
|
|
905
|
+
document.getElementById('PRSP-Read-EditButton').classList.add('record-button-bar-hidden');
|
|
906
|
+
document.getElementById('PRSP-Read-SaveButton').classList.add('record-button-bar-hidden');
|
|
907
|
+
document.getElementById('PRSP-Read-CancelButton').classList.add('record-button-bar-hidden');
|
|
908
|
+
}
|
|
850
909
|
else
|
|
851
910
|
{
|
|
852
911
|
document.getElementById('PRSP-Read-EditButton').classList.remove('record-button-bar-hidden');
|