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.1",
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.373",
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
- this.pict.EntityProvider.cacheConnectedEntityRecords(recordsReturn, IDFields, ['User', 'User'], false, () =>
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: false,
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