pict-section-recordset 1.9.4 → 1.9.6
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
|
@@ -66,6 +66,40 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
66
66
|
return this._EntityProvider;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Fetch (and cache) the DISTINCT values of a column present in this recordset's data, via
|
|
71
|
+
* Meadow's `<Entity>s/Distinct/<Column>` endpoint. Drives the `ScopeToRecordSet` filter knob:
|
|
72
|
+
* an entity picker can be limited to `FBL~<Column>~INN~<these values>` so it only lists the
|
|
73
|
+
* entities the data actually references, not the whole remote table. Cached per column.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} pColumn @param {(pError: Error|null, pValues: Array<any>) => void} fCallback
|
|
76
|
+
*/
|
|
77
|
+
getRecordSetColumnDistinct(pColumn, fCallback)
|
|
78
|
+
{
|
|
79
|
+
this._scopeDistinctCache = this._scopeDistinctCache || {};
|
|
80
|
+
if (Array.isArray(this._scopeDistinctCache[pColumn]))
|
|
81
|
+
{
|
|
82
|
+
return fCallback(null, this._scopeDistinctCache[pColumn]);
|
|
83
|
+
}
|
|
84
|
+
if (!this.options.Entity || !this.entityProvider || !this.entityProvider.restClient)
|
|
85
|
+
{
|
|
86
|
+
return fCallback(new Error('RecordSet provider cannot resolve a distinct request (missing Entity or rest client).'), []);
|
|
87
|
+
}
|
|
88
|
+
const tmpURL = `${this.options.URLPrefix || ''}${this.options.Entity}s/Distinct/${pColumn}`;
|
|
89
|
+
this.entityProvider.restClient.getJSON(tmpURL, (pError, pResponse, pBody) =>
|
|
90
|
+
{
|
|
91
|
+
if (pError || (pResponse && pResponse.statusCode > 299) || !Array.isArray(pBody))
|
|
92
|
+
{
|
|
93
|
+
this.pict.log.warn(`RecordSet [${this.options.RecordSet || this.options.Entity}] distinct fetch for [${pColumn}] failed; the scoped filter falls back to unscoped.`, { Error: pError && pError.message, URL: tmpURL });
|
|
94
|
+
this._scopeDistinctCache[pColumn] = [];
|
|
95
|
+
return fCallback(pError || new Error('distinct fetch returned a non-array'), []);
|
|
96
|
+
}
|
|
97
|
+
const tmpValues = [ ...new Set(pBody.map((pRecord) => pRecord && pRecord[pColumn]).filter((pValue) => pValue != null)) ];
|
|
98
|
+
this._scopeDistinctCache[pColumn] = tmpValues;
|
|
99
|
+
return fCallback(null, tmpValues);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
69
103
|
/**
|
|
70
104
|
* @return {Array<string>} - The fields to ignore for filter availability.
|
|
71
105
|
*/
|
|
@@ -674,6 +674,23 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
674
674
|
const tmpDescriptor = tmpProvider.getFilterClauseSchemaForKey(pMount.Field)?.AvailableClauses?.find?.((pClause) => pClause.ClauseKey === pMount.ClauseKey);
|
|
675
675
|
if (!tmpDescriptor || !tmpDescriptor.RemoteTable) { return; }
|
|
676
676
|
const tmpSearchFields = Array.isArray(tmpDescriptor.ExternalFilterByColumns) && tmpDescriptor.ExternalFilterByColumns.length > 0 ? tmpDescriptor.ExternalFilterByColumns : [ 'Name' ];
|
|
677
|
+
// ScopeToRecordSet knob: limit the picker to the values present in this recordset's data
|
|
678
|
+
// (FBL~<col>~INN~<distinct>) so it doesn't list the whole remote table. Pre-fetch the
|
|
679
|
+
// distinct set, then re-mount once it resolves so the first open is already scoped.
|
|
680
|
+
let tmpScopeBaseFilter = undefined;
|
|
681
|
+
if (tmpDescriptor.ScopeToRecordSet && tmpDescriptor.FilterByColumn && typeof tmpProvider.getRecordSetColumnDistinct === 'function')
|
|
682
|
+
{
|
|
683
|
+
const tmpScopeColumn = tmpDescriptor.FilterByColumn;
|
|
684
|
+
if (!tmpProvider._scopeDistinctCache || !Array.isArray(tmpProvider._scopeDistinctCache[tmpScopeColumn]))
|
|
685
|
+
{
|
|
686
|
+
tmpProvider.getRecordSetColumnDistinct(tmpScopeColumn, () => this._mountQuickFilterEntity(pRecordSet, pViewContext, pMount));
|
|
687
|
+
}
|
|
688
|
+
tmpScopeBaseFilter = () =>
|
|
689
|
+
{
|
|
690
|
+
const tmpVals = (tmpProvider._scopeDistinctCache || {})[tmpScopeColumn];
|
|
691
|
+
return (Array.isArray(tmpVals) && tmpVals.length > 0) ? `FBL~${tmpScopeColumn}~INN~${tmpVals.join(',')}` : '';
|
|
692
|
+
};
|
|
693
|
+
}
|
|
677
694
|
const tmpView = tmpPickerProvider.createEntityPicker(`Quick-Picker-${pRecordSet}-${pMount.Field}`,
|
|
678
695
|
{
|
|
679
696
|
DestinationAddress: `#${pMount.HostID}`,
|
|
@@ -688,6 +705,7 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
688
705
|
TextTemplate: tmpDescriptor.EntityListEntryTemplate || undefined,
|
|
689
706
|
Placeholder: `Select ${pMount.Label}…`,
|
|
690
707
|
OnChange: (pValue) => this.applyQuickFilterEntity(pRecordSet, pViewContext, pMount.Field, pMount.ClauseKey, pValue),
|
|
708
|
+
BaseFilter: tmpScopeBaseFilter,
|
|
691
709
|
});
|
|
692
710
|
if (!tmpView) { return; }
|
|
693
711
|
tmpView.render();
|
|
@@ -127,6 +127,25 @@ class ViewRecordSetSUBSETFilterEntityReferenceBase extends ViewRecordSetSUBSETFi
|
|
|
127
127
|
*/
|
|
128
128
|
getContextScopeFilter(pClause)
|
|
129
129
|
{
|
|
130
|
+
// ScopeToRecordSet knob: scope the entity search to the values present in the recordset's
|
|
131
|
+
// data (FBL~<col>~INN~<distinct>), fetched + cached on the recordset provider. The first
|
|
132
|
+
// search may be unscoped until the fetch resolves; subsequent searches are scoped.
|
|
133
|
+
if (pClause && pClause.ScopeToRecordSet && pClause.FilterByColumn)
|
|
134
|
+
{
|
|
135
|
+
const tmpProvider = this.pict.providers['RSP-Provider-' + pClause.RecordSet];
|
|
136
|
+
if (tmpProvider && typeof tmpProvider.getRecordSetColumnDistinct === 'function')
|
|
137
|
+
{
|
|
138
|
+
if (!tmpProvider._scopeDistinctCache || !Array.isArray(tmpProvider._scopeDistinctCache[pClause.FilterByColumn]))
|
|
139
|
+
{
|
|
140
|
+
tmpProvider.getRecordSetColumnDistinct(pClause.FilterByColumn, () => {});
|
|
141
|
+
}
|
|
142
|
+
const tmpVals = (tmpProvider._scopeDistinctCache || {})[pClause.FilterByColumn];
|
|
143
|
+
if (Array.isArray(tmpVals) && tmpVals.length > 0)
|
|
144
|
+
{
|
|
145
|
+
return `FBL~${pClause.FilterByColumn}~INN~${tmpVals.join(',')}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
130
149
|
return pClause.ContextScopeFilter || '';
|
|
131
150
|
}
|
|
132
151
|
|
|
@@ -51,9 +51,9 @@ const _DEFAULT_CONFIGURATION__List = (
|
|
|
51
51
|
<section id="PRSP_Filters_Container">
|
|
52
52
|
{~FV:PRSP-Filters:List~}
|
|
53
53
|
</section>
|
|
54
|
-
{~V:PRSP-List-PaginationTop~}
|
|
55
|
-
{~V:PRSP-List-RecordList~}
|
|
56
|
-
{~V:PRSP-List-PaginationBottom~}
|
|
54
|
+
<div id="PRSP_PaginationTop_Container">{~V:PRSP-List-PaginationTop~}</div>
|
|
55
|
+
<div id="PRSP_RecordList_Container">{~V:PRSP-List-RecordList~}</div>
|
|
56
|
+
<div id="PRSP_PaginationBottom_Container">{~V:PRSP-List-PaginationBottom~}</div>
|
|
57
57
|
</section>
|
|
58
58
|
<!-- DefaultPackage end view template: [PRSP-List-Template] -->
|
|
59
59
|
`
|
|
@@ -106,6 +106,11 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
106
106
|
recordListEntry: null,
|
|
107
107
|
paginationBottom: null
|
|
108
108
|
};
|
|
109
|
+
|
|
110
|
+
// Identity (`RecordSet::FilterString::FilterExperience`) of the list currently painted into the DOM.
|
|
111
|
+
// When a route only changes the page (Offset/PageSize) and this still matches, we re-render just the
|
|
112
|
+
// rows + pagination instead of the whole view — see handleRecordSetListRoute / _paintRecordList.
|
|
113
|
+
this._renderedListIdentity = null;
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
handleRecordSetListRoute(pRoutePayload)
|
|
@@ -131,7 +136,17 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
131
136
|
const tmpOffset = pRoutePayload.data.Offset ? pRoutePayload.data.Offset : 0;
|
|
132
137
|
const tmpPageSize = pRoutePayload.data.PageSize ? pRoutePayload.data.PageSize : 100;
|
|
133
138
|
|
|
134
|
-
|
|
139
|
+
// Surgical page render: when only the page changed (same RecordSet, FilterString, and
|
|
140
|
+
// FilterExperience), re-render just the rows + pagination rather than the whole list view. A full
|
|
141
|
+
// re-render rebuilds the filter view — including all of its picker/control state — which is by far
|
|
142
|
+
// the most expensive part of a list paint and entirely wasted when only paging. Requires the list to
|
|
143
|
+
// already be in the DOM (its rows container exists); otherwise we fall through to a full render.
|
|
144
|
+
const tmpListIdentity = `${pRoutePayload.data.RecordSet}::${tmpFilterString}::${tmpFilterExperience}`;
|
|
145
|
+
const tmpRenderBodyOnly = (this._renderedListIdentity === tmpListIdentity)
|
|
146
|
+
&& (this.pict.ContentAssignment.getElement('#PRSP_RecordList_Container').length > 0);
|
|
147
|
+
this._renderedListIdentity = tmpListIdentity;
|
|
148
|
+
|
|
149
|
+
return this.renderList(tmpProviderConfiguration, tmpProviderHash, tmpFilterString, tmpFilterExperience, tmpOffset, tmpPageSize, tmpRenderBodyOnly);
|
|
135
150
|
}
|
|
136
151
|
|
|
137
152
|
addRoutes(pPictRouter)
|
|
@@ -174,8 +189,25 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
174
189
|
{
|
|
175
190
|
return;
|
|
176
191
|
}
|
|
192
|
+
// When the list is already on screen, scope the spinner to just the rows area so the title,
|
|
193
|
+
// filters, and pagination stay put (and the expensive filter view isn't disturbed). On the first
|
|
194
|
+
// render the rows container doesn't exist yet, so fall back to the whole list destination.
|
|
195
|
+
const tmpRowsElements = this.pict.ContentAssignment.getElement('#PRSP_RecordList_Container');
|
|
196
|
+
const tmpRowsContainerPresent = tmpRowsElements.length > 0;
|
|
197
|
+
const tmpDestination = tmpRowsContainerPresent ? '#PRSP_RecordList_Container' : pRecordListData.RenderDestination;
|
|
198
|
+
if (tmpRowsContainerPresent)
|
|
199
|
+
{
|
|
200
|
+
// Pin the rows area to its current height before swapping in the (short) spinner, so the page
|
|
201
|
+
// doesn't collapse and yank the content below it — pagination, the page's footer/colored fill —
|
|
202
|
+
// up into the fold and back. The floor is released once the real rows render (see _paintRecordList).
|
|
203
|
+
const tmpCurrentHeight = tmpRowsElements[0].offsetHeight;
|
|
204
|
+
if (tmpCurrentHeight > 0)
|
|
205
|
+
{
|
|
206
|
+
tmpRowsElements[0].style.minHeight = `${ tmpCurrentHeight }px`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
177
209
|
this.pict.CSSMap.injectCSS();
|
|
178
|
-
this.pict.ContentAssignment.assignContent(
|
|
210
|
+
this.pict.ContentAssignment.assignContent(tmpDestination, this.pict.parseTemplateByHash('PRSP-List-LoadingShell', pRecordListData));
|
|
179
211
|
}
|
|
180
212
|
catch (pError)
|
|
181
213
|
{
|
|
@@ -230,10 +262,11 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
230
262
|
* @param {string} pSerializedFilterExperience
|
|
231
263
|
* @param {number} pOffset
|
|
232
264
|
* @param {number} pPageSize
|
|
265
|
+
* @param {boolean} [pBodyOnly] - When true, re-render only the rows + pagination (page change), leaving the filter view intact.
|
|
233
266
|
*
|
|
234
267
|
* @return {Promise<void>}
|
|
235
268
|
*/
|
|
236
|
-
async renderList(pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize)
|
|
269
|
+
async renderList(pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize, pBodyOnly)
|
|
237
270
|
{
|
|
238
271
|
// Get the records
|
|
239
272
|
if (!(pProviderHash in this.pict.providers))
|
|
@@ -252,7 +285,7 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
252
285
|
}
|
|
253
286
|
else
|
|
254
287
|
{
|
|
255
|
-
return this.renderListFromManifest(tmpManifest, pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize);
|
|
288
|
+
return this.renderListFromManifest(tmpManifest, pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize, pBodyOnly);
|
|
256
289
|
}
|
|
257
290
|
}
|
|
258
291
|
|
|
@@ -471,25 +504,7 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
471
504
|
}
|
|
472
505
|
tmpRecordListData = this.onBeforeRenderList(tmpRecordListData);
|
|
473
506
|
|
|
474
|
-
this.
|
|
475
|
-
function (pError)
|
|
476
|
-
{
|
|
477
|
-
if (pError)
|
|
478
|
-
{
|
|
479
|
-
this.pict.log.error(`RecordSetList: Error rendering list ${pError}`, tmpRecordListData);
|
|
480
|
-
return false;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (this.pict.LogNoisiness > 0)
|
|
484
|
-
{
|
|
485
|
-
this.pict.log.info(`RecordSetList: Rendered list ${tmpRecordListData.RecordSet} with ${tmpRecordListData.Records.Records.length} records.`, tmpRecordListData);
|
|
486
|
-
}
|
|
487
|
-
else
|
|
488
|
-
{
|
|
489
|
-
this.pict.log.info(`RecordSetList: Rendered list ${tmpRecordListData.RecordSet} with ${tmpRecordListData.Records.Records.length} records.`);
|
|
490
|
-
}
|
|
491
|
-
return true;
|
|
492
|
-
}.bind(this));
|
|
507
|
+
this._paintRecordList(tmpRecordListData, pBodyOnly);
|
|
493
508
|
}
|
|
494
509
|
|
|
495
510
|
/**
|
|
@@ -500,10 +515,11 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
500
515
|
* @param {string} pSerializedFilterExperience
|
|
501
516
|
* @param {number} pOffset
|
|
502
517
|
* @param {number} pPageSize
|
|
518
|
+
* @param {boolean} [pBodyOnly] - When true, re-render only the rows + pagination (page change), leaving the filter view intact.
|
|
503
519
|
*
|
|
504
520
|
* @return {Promise<void>}
|
|
505
521
|
*/
|
|
506
|
-
async renderListFromManifest(pManifest, pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize)
|
|
522
|
+
async renderListFromManifest(pManifest, pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize, pBodyOnly)
|
|
507
523
|
{
|
|
508
524
|
if (!pRecordSetConfiguration)
|
|
509
525
|
{
|
|
@@ -738,23 +754,64 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
738
754
|
|
|
739
755
|
this.pict.providers.DynamicRecordsetSolver.solveDashboard(pManifest, tmpRecordListData.Records.Records);
|
|
740
756
|
|
|
741
|
-
this.
|
|
742
|
-
|
|
757
|
+
this._paintRecordList(tmpRecordListData, pBodyOnly);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Paint the computed record-list data into the DOM.
|
|
762
|
+
*
|
|
763
|
+
* Full render (pBodyOnly falsy): render the whole `PRSP_Renderable_List` (title, header, filters,
|
|
764
|
+
* pagination, rows) into the list destination — the original behavior.
|
|
765
|
+
*
|
|
766
|
+
* Body-only render (pBodyOnly true): only the page changed, so re-render just the rows and the two
|
|
767
|
+
* pagination strips into their stable containers, leaving the filter view (and its picker/control state)
|
|
768
|
+
* completely untouched. Each child is rendered with the freshly-computed record passed as an object, so
|
|
769
|
+
* it produces exactly what the inline `{~V:~}` render would have.
|
|
770
|
+
*
|
|
771
|
+
* @param {Record<string, any>} pRecordListData - The fully-computed list data (records, pagination, cells).
|
|
772
|
+
* @param {boolean} [pBodyOnly] - When true, surgically re-render only rows + pagination.
|
|
773
|
+
* @return {void}
|
|
774
|
+
*/
|
|
775
|
+
_paintRecordList(pRecordListData, pBodyOnly)
|
|
776
|
+
{
|
|
777
|
+
const fLogRendered = function (pError)
|
|
778
|
+
{
|
|
779
|
+
if (pError)
|
|
743
780
|
{
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
781
|
+
this.pict.log.error(`RecordSetList: Error rendering list ${pError}`, pRecordListData);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (this.pict.LogNoisiness > 0)
|
|
785
|
+
{
|
|
786
|
+
this.pict.log.info(`RecordSetList: Rendered list ${pRecordListData.RecordSet} with ${pRecordListData.Records.Records.length} records.`, pRecordListData);
|
|
787
|
+
}
|
|
788
|
+
else
|
|
789
|
+
{
|
|
790
|
+
this.pict.log.info(`RecordSetList: Rendered list ${pRecordListData.RecordSet} with ${pRecordListData.Records.Records.length} records.`);
|
|
791
|
+
}
|
|
792
|
+
}.bind(this);
|
|
749
793
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
794
|
+
if (!pBodyOnly)
|
|
795
|
+
{
|
|
796
|
+
this.renderAsync('PRSP_Renderable_List', pRecordListData.RenderDestination, pRecordListData, fLogRendered);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Page-only change: re-render the two pagination strips (current page + "showing X of Y") and the
|
|
801
|
+
// rows, each into its own stable container. The filter view, title, and header list are left as-is.
|
|
802
|
+
this.childViews.paginationTop.renderAsync('PRSP_Renderable_PaginationTop', '#PRSP_PaginationTop_Container', pRecordListData, null, () => { });
|
|
803
|
+
this.childViews.paginationBottom.renderAsync('PRSP_Renderable_PaginationBottom', '#PRSP_PaginationBottom_Container', pRecordListData, null, () => { });
|
|
804
|
+
this.childViews.recordList.renderAsync('PRSP_Renderable_RecordList', '#PRSP_RecordList_Container', pRecordListData, null,
|
|
805
|
+
function (pError)
|
|
806
|
+
{
|
|
807
|
+
// Release the height floor that was pinned while the spinner showed (see _projectLoadingShell),
|
|
808
|
+
// now that the real rows are back in and the container can size to its content again.
|
|
809
|
+
const tmpRows = this.pict.ContentAssignment.getElement('#PRSP_RecordList_Container');
|
|
810
|
+
if (tmpRows.length)
|
|
755
811
|
{
|
|
756
|
-
|
|
812
|
+
tmpRows[0].style.minHeight = '';
|
|
757
813
|
}
|
|
814
|
+
fLogRendered(pError);
|
|
758
815
|
}.bind(this));
|
|
759
816
|
}
|
|
760
817
|
|