pict-section-recordset 1.9.1 → 1.9.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-section-recordset",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
4
4
|
"description": "Pict dynamic record set management views",
|
|
5
5
|
"main": "source/Pict-Section-RecordSet.js",
|
|
6
6
|
"files": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"browser-env": "^3.3.0",
|
|
38
38
|
"eslint": "^9.28.0",
|
|
39
39
|
"jquery": "^3.7.1",
|
|
40
|
-
"pict": "^1.0.
|
|
40
|
+
"pict": "^1.0.374",
|
|
41
41
|
"pict-application": "^1.0.34",
|
|
42
42
|
"pict-docuserve": "^1.4.19",
|
|
43
43
|
"pict-service-commandlineutility": "^1.0.19",
|
|
@@ -192,6 +192,63 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
192
192
|
return [ tmpClauses, tmpExperience ];
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Derive the Lite `ExtraColumns` for a list fetch from the manifest's displayed
|
|
197
|
+
* columns. Lite already returns the ID-prefixed, GUID-prefixed, CreatingIDUser and
|
|
198
|
+
* UpdateDate fields plus a computed Value, so we only request the remaining scalar
|
|
199
|
+
* display columns — and only ones that are real, non-blob schema columns. Returns
|
|
200
|
+
* [] (caller then does a safe full fetch) if the manifest columns or schema are
|
|
201
|
+
* unavailable.
|
|
202
|
+
* @param {string} pEntity - The entity being listed.
|
|
203
|
+
* @param {Record<string, any>} pOptions - The list options (carries RecordSetConfiguration).
|
|
204
|
+
* @return {Array<string>} The ExtraColumns to request.
|
|
205
|
+
*/
|
|
206
|
+
_deriveLiteExtraColumns(pEntity, pOptions)
|
|
207
|
+
{
|
|
208
|
+
const tmpConfig = pOptions && pOptions.RecordSetConfiguration;
|
|
209
|
+
let tmpDescriptors = null;
|
|
210
|
+
if (tmpConfig && this.pict.PictSectionRecordSet && this.pict.PictSectionRecordSet.manifestDefinitions)
|
|
211
|
+
{
|
|
212
|
+
const tmpManifestHash = tmpConfig.RecordSetListDefaultManifest || (Array.isArray(tmpConfig.RecordSetListManifests) ? tmpConfig.RecordSetListManifests[0] : null);
|
|
213
|
+
const tmpManifest = tmpManifestHash ? this.pict.PictSectionRecordSet.manifestDefinitions[tmpManifestHash] : null;
|
|
214
|
+
tmpDescriptors = (tmpManifest && tmpManifest.Descriptors) || null;
|
|
215
|
+
}
|
|
216
|
+
const tmpSchemaColumns = (this._Schema && this._Schema.MeadowSchema && Array.isArray(this._Schema.MeadowSchema.Schema)) ? this._Schema.MeadowSchema.Schema : [];
|
|
217
|
+
if (!tmpDescriptors || tmpSchemaColumns.length < 1)
|
|
218
|
+
{
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
const tmpColumnType = {};
|
|
222
|
+
for (const tmpColumn of tmpSchemaColumns)
|
|
223
|
+
{
|
|
224
|
+
if (tmpColumn && tmpColumn.Column)
|
|
225
|
+
{
|
|
226
|
+
tmpColumnType[tmpColumn.Column] = tmpColumn.Type;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const tmpBlobTypes = { 'Text': true, 'JSON': true };
|
|
230
|
+
const tmpColumns = [];
|
|
231
|
+
for (const tmpKey of Object.keys(tmpDescriptors))
|
|
232
|
+
{
|
|
233
|
+
// ID*/GUID*/owner/update come back in every Lite record for free.
|
|
234
|
+
if (tmpKey.startsWith('ID') || tmpKey.startsWith('GUID') || tmpKey === 'CreatingIDUser' || tmpKey === 'UpdateDate')
|
|
235
|
+
{
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
// Only request real, non-blob columns (a computed/templated manifest column
|
|
239
|
+
// that is not a DB column would otherwise error the query).
|
|
240
|
+
if (!(tmpKey in tmpColumnType) || tmpBlobTypes[tmpColumnType[tmpKey]])
|
|
241
|
+
{
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (!tmpColumns.includes(tmpKey))
|
|
245
|
+
{
|
|
246
|
+
tmpColumns.push(tmpKey);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return tmpColumns;
|
|
250
|
+
}
|
|
251
|
+
|
|
195
252
|
/**
|
|
196
253
|
* Read records from the provider.
|
|
197
254
|
*
|
|
@@ -205,6 +262,30 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
205
262
|
throw new Error('Entity is not defined in the provider options.');
|
|
206
263
|
}
|
|
207
264
|
const tmpEntity = pOptions.Entity || this.options.Entity;
|
|
265
|
+
|
|
266
|
+
// Lite projection (opt-in via RecordSetListLiteFetch): request only the columns
|
|
267
|
+
// the manifest displays so the list stops pulling blob columns (FormData, etc.).
|
|
268
|
+
// Lite (partial) records are NOT written to the entity cache (see getEntitySetPage) —
|
|
269
|
+
// the list renders them straight from state — so they can never poison the global
|
|
270
|
+
// cache that full-record consumers (row-click View, {~E:~}) rely on. The list's
|
|
271
|
+
// reference entities (Project/User) are full records and stay in the global cache,
|
|
272
|
+
// batched reliably by the connected-entity prefetch + the stale-read prune fix.
|
|
273
|
+
const tmpLiteFetch = (this.options.RecordSetListLiteFetch === true) || !!(pOptions && pOptions.RecordSetConfiguration && pOptions.RecordSetConfiguration.RecordSetListLiteFetch);
|
|
274
|
+
let tmpProjection = null;
|
|
275
|
+
if (tmpLiteFetch)
|
|
276
|
+
{
|
|
277
|
+
// Ensure the entity schema is loaded so we only request real, non-blob columns.
|
|
278
|
+
if (!this._Schema)
|
|
279
|
+
{
|
|
280
|
+
await this.getRecordSchema();
|
|
281
|
+
}
|
|
282
|
+
const tmpExtraColumns = this._deriveLiteExtraColumns(tmpEntity, pOptions);
|
|
283
|
+
if (tmpExtraColumns.length > 0)
|
|
284
|
+
{
|
|
285
|
+
tmpProjection = { Mode: 'LiteExtended', ExtraColumns: tmpExtraColumns };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
208
289
|
if (this.pict.LogNoisiness > 1)
|
|
209
290
|
{
|
|
210
291
|
this.pict.log.info(`Reading ${tmpEntity} records`, { Options: pOptions });
|
|
@@ -212,6 +293,10 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
212
293
|
return new Promise((resolve, reject) =>
|
|
213
294
|
{
|
|
214
295
|
const [ tmpClauses, tmpExperience ] = this._prepareFilterState(tmpEntity, pOptions);
|
|
296
|
+
if (tmpProjection)
|
|
297
|
+
{
|
|
298
|
+
tmpExperience.Projection = tmpProjection;
|
|
299
|
+
}
|
|
215
300
|
if (this.options.FilterEndpointOverride)
|
|
216
301
|
{
|
|
217
302
|
// Call the filtering endpoint with the clauses and experience.
|
|
@@ -237,7 +322,7 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
237
322
|
}
|
|
238
323
|
}
|
|
239
324
|
}
|
|
240
|
-
this.pict.EntityProvider.cacheConnectedEntityRecordsWithoutCount(recordsReturn, IDFields, ['User', 'User'], false, () =>
|
|
325
|
+
this.pict.EntityProvider.cacheConnectedEntityRecordsWithoutCount(recordsReturn, IDFields, ['User', 'User'], false, () =>
|
|
241
326
|
{
|
|
242
327
|
resolve({ Records: recordsReturn, Facets: { } });
|
|
243
328
|
});
|
|
@@ -262,7 +347,12 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
262
347
|
}
|
|
263
348
|
}
|
|
264
349
|
}
|
|
265
|
-
|
|
350
|
+
// Use the NoCount (lazy-page) batch: counts are very costly in this MySQL,
|
|
351
|
+
// and the count-based variant serializes one slow COUNT per reference entity,
|
|
352
|
+
// stalling the queue so later entities (e.g. Project) never batch and fall
|
|
353
|
+
// back to per-row fetches. NoCount fetches each batch in one paged request
|
|
354
|
+
// with real concurrency (maxOperations).
|
|
355
|
+
this.pict.EntityProvider.cacheConnectedEntityRecordsWithoutCount(recordsReturn, IDFields, ['User', 'User'], false, () =>
|
|
266
356
|
{
|
|
267
357
|
resolve({ Records: recordsReturn, Facets: { } });
|
|
268
358
|
});
|
|
@@ -371,6 +461,11 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
371
461
|
}
|
|
372
462
|
// A new record changes the total; drop the cached count so the next render re-counts.
|
|
373
463
|
this._RecordSetCountCache = null;
|
|
464
|
+
// Drop this list's scoped cache too, so the next render re-fetches fresh.
|
|
465
|
+
if (typeof this.pict.EntityProvider.clearScope === 'function')
|
|
466
|
+
{
|
|
467
|
+
this.pict.EntityProvider.clearScope(`RSList::${ this.options.RecordSet || this.options.Entity }`);
|
|
468
|
+
}
|
|
374
469
|
resolve(result);
|
|
375
470
|
});
|
|
376
471
|
});
|
|
@@ -400,6 +495,11 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
400
495
|
}
|
|
401
496
|
// An edit can move a record in or out of the active filter; drop the cached count to be safe.
|
|
402
497
|
this._RecordSetCountCache = null;
|
|
498
|
+
// Drop this list's scoped cache too, so the next render re-fetches fresh.
|
|
499
|
+
if (typeof this.pict.EntityProvider.clearScope === 'function')
|
|
500
|
+
{
|
|
501
|
+
this.pict.EntityProvider.clearScope(`RSList::${ this.options.RecordSet || this.options.Entity }`);
|
|
502
|
+
}
|
|
403
503
|
resolve(result);
|
|
404
504
|
});
|
|
405
505
|
});
|
|
@@ -429,6 +529,11 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
429
529
|
}
|
|
430
530
|
// A delete changes the total; drop the cached count so the next render re-counts.
|
|
431
531
|
this._RecordSetCountCache = null;
|
|
532
|
+
// Drop this list's scoped cache too, so the next render re-fetches fresh.
|
|
533
|
+
if (typeof this.pict.EntityProvider.clearScope === 'function')
|
|
534
|
+
{
|
|
535
|
+
this.pict.EntityProvider.clearScope(`RSList::${ this.options.RecordSet || this.options.Entity }`);
|
|
536
|
+
}
|
|
432
537
|
resolve(result);
|
|
433
538
|
});
|
|
434
539
|
});
|
|
@@ -31,7 +31,12 @@ const _DEFAULT_CONFIGURATION__List = (
|
|
|
31
31
|
AutoSolveWithApp: false,
|
|
32
32
|
AutoSolveOrdinal: 0,
|
|
33
33
|
|
|
34
|
-
CSS:
|
|
34
|
+
CSS: /*css*/`
|
|
35
|
+
.prsp-list-loading { display: flex; align-items: center; justify-content: center; min-height: 240px; width: 100%; }
|
|
36
|
+
.prsp-list-loading-inner { display: inline-flex; align-items: center; gap: 0.6em; color: var(--theme-color-text-muted, #64748b); font-size: 1.05rem; }
|
|
37
|
+
.prsp-list-spinner { display: inline-flex; animation: prsp-list-spin 0.9s linear infinite; }
|
|
38
|
+
@keyframes prsp-list-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
39
|
+
`,
|
|
35
40
|
CSSPriority: 500,
|
|
36
41
|
|
|
37
42
|
Templates:
|
|
@@ -57,6 +62,17 @@ const _DEFAULT_CONFIGURATION__List = (
|
|
|
57
62
|
Hash: 'PRSP-List-Template-Record',
|
|
58
63
|
Template: /*html*/`
|
|
59
64
|
<!-- DefaultPackage end view template: [PRSP-List-Template] -->
|
|
65
|
+
`
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
Hash: 'PRSP-List-LoadingShell',
|
|
69
|
+
Template: /*html*/`
|
|
70
|
+
<section id="PRSP_List_Loading" class="prsp-list-loading">
|
|
71
|
+
<div class="prsp-list-loading-inner">
|
|
72
|
+
<span class="prsp-list-spinner" aria-hidden="true">{~I:Refresh~}</span>
|
|
73
|
+
<span class="prsp-list-loading-label">Loading…</span>
|
|
74
|
+
</div>
|
|
75
|
+
</section>
|
|
60
76
|
`
|
|
61
77
|
}
|
|
62
78
|
],
|
|
@@ -138,6 +154,36 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
138
154
|
return pRecordListData;
|
|
139
155
|
}
|
|
140
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Paint a loading shell into the list destination synchronously, before the data
|
|
159
|
+
* fetch, so the previous page doesn't sit silently while a slow query runs. The
|
|
160
|
+
* real list render (RenderMethod 'replace' into the same destination) overwrites
|
|
161
|
+
* it when data arrives. Opt out with RecordSetListShowLoadingShell:false.
|
|
162
|
+
* @param {Record<string, any>} pRecordListData
|
|
163
|
+
*/
|
|
164
|
+
_projectLoadingShell(pRecordListData)
|
|
165
|
+
{
|
|
166
|
+
try
|
|
167
|
+
{
|
|
168
|
+
const tmpConfig = pRecordListData && pRecordListData.RecordSetConfiguration;
|
|
169
|
+
if (tmpConfig && tmpConfig.RecordSetListShowLoadingShell === false)
|
|
170
|
+
{
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!pRecordListData || !pRecordListData.RenderDestination)
|
|
174
|
+
{
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.pict.CSSMap.injectCSS();
|
|
178
|
+
this.pict.ContentAssignment.assignContent(pRecordListData.RenderDestination, this.pict.parseTemplateByHash('PRSP-List-LoadingShell', pRecordListData));
|
|
179
|
+
}
|
|
180
|
+
catch (pError)
|
|
181
|
+
{
|
|
182
|
+
// The loading shell is purely cosmetic; never let it break the list render.
|
|
183
|
+
this.log.warn(`RecordSetList: loading shell render failed: ${ pError && pError.message }`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
141
187
|
dynamicallyGenerateColumns(pRecordListData)
|
|
142
188
|
{
|
|
143
189
|
pRecordListData.TableCells = [];
|
|
@@ -240,6 +286,9 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
240
286
|
};
|
|
241
287
|
|
|
242
288
|
// TODO: There are still problems with the way these have nested data. Discuss how we might move that around
|
|
289
|
+
// Paint a loading shell before the (potentially slow) fetch so the prior page
|
|
290
|
+
// doesn't sit silently; the real list render replaces it when data arrives.
|
|
291
|
+
this._projectLoadingShell(tmpRecordListData);
|
|
243
292
|
// Fetch the records
|
|
244
293
|
const [ tmpRecords, tmpTotalRecordCount, tmpRecordSchema ] = await Promise.all([
|
|
245
294
|
this.pict.providers[pProviderHash].getRecords(tmpRecordListData),
|
|
@@ -504,6 +553,9 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
504
553
|
};
|
|
505
554
|
|
|
506
555
|
// TODO: There are still problems with the way these have nested data. Discuss how we might move that around
|
|
556
|
+
// Paint a loading shell before the (potentially slow) fetch so the prior page
|
|
557
|
+
// doesn't sit silently; the real list render replaces it when data arrives.
|
|
558
|
+
this._projectLoadingShell(tmpRecordListData);
|
|
507
559
|
// Fetch the records
|
|
508
560
|
tmpRecordListData.Records = await this.pict.providers[pProviderHash].getDecoratedRecords(tmpRecordListData);
|
|
509
561
|
// Get the total record count
|