pict-section-recordset 1.9.4 → 1.9.5
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,13 @@ 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 tmpRowsContainerPresent = this.pict.ContentAssignment.getElement('#PRSP_RecordList_Container').length > 0;
|
|
196
|
+
const tmpDestination = tmpRowsContainerPresent ? '#PRSP_RecordList_Container' : pRecordListData.RenderDestination;
|
|
177
197
|
this.pict.CSSMap.injectCSS();
|
|
178
|
-
this.pict.ContentAssignment.assignContent(
|
|
198
|
+
this.pict.ContentAssignment.assignContent(tmpDestination, this.pict.parseTemplateByHash('PRSP-List-LoadingShell', pRecordListData));
|
|
179
199
|
}
|
|
180
200
|
catch (pError)
|
|
181
201
|
{
|
|
@@ -230,10 +250,11 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
230
250
|
* @param {string} pSerializedFilterExperience
|
|
231
251
|
* @param {number} pOffset
|
|
232
252
|
* @param {number} pPageSize
|
|
253
|
+
* @param {boolean} [pBodyOnly] - When true, re-render only the rows + pagination (page change), leaving the filter view intact.
|
|
233
254
|
*
|
|
234
255
|
* @return {Promise<void>}
|
|
235
256
|
*/
|
|
236
|
-
async renderList(pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize)
|
|
257
|
+
async renderList(pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize, pBodyOnly)
|
|
237
258
|
{
|
|
238
259
|
// Get the records
|
|
239
260
|
if (!(pProviderHash in this.pict.providers))
|
|
@@ -252,7 +273,7 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
252
273
|
}
|
|
253
274
|
else
|
|
254
275
|
{
|
|
255
|
-
return this.renderListFromManifest(tmpManifest, pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize);
|
|
276
|
+
return this.renderListFromManifest(tmpManifest, pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize, pBodyOnly);
|
|
256
277
|
}
|
|
257
278
|
}
|
|
258
279
|
|
|
@@ -471,25 +492,7 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
471
492
|
}
|
|
472
493
|
tmpRecordListData = this.onBeforeRenderList(tmpRecordListData);
|
|
473
494
|
|
|
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));
|
|
495
|
+
this._paintRecordList(tmpRecordListData, pBodyOnly);
|
|
493
496
|
}
|
|
494
497
|
|
|
495
498
|
/**
|
|
@@ -500,10 +503,11 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
500
503
|
* @param {string} pSerializedFilterExperience
|
|
501
504
|
* @param {number} pOffset
|
|
502
505
|
* @param {number} pPageSize
|
|
506
|
+
* @param {boolean} [pBodyOnly] - When true, re-render only the rows + pagination (page change), leaving the filter view intact.
|
|
503
507
|
*
|
|
504
508
|
* @return {Promise<void>}
|
|
505
509
|
*/
|
|
506
|
-
async renderListFromManifest(pManifest, pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize)
|
|
510
|
+
async renderListFromManifest(pManifest, pRecordSetConfiguration, pProviderHash, pFilterString, pSerializedFilterExperience, pOffset, pPageSize, pBodyOnly)
|
|
507
511
|
{
|
|
508
512
|
if (!pRecordSetConfiguration)
|
|
509
513
|
{
|
|
@@ -738,24 +742,54 @@ class viewRecordSetList extends libPictRecordSetRecordView
|
|
|
738
742
|
|
|
739
743
|
this.pict.providers.DynamicRecordsetSolver.solveDashboard(pManifest, tmpRecordListData.Records.Records);
|
|
740
744
|
|
|
741
|
-
this.
|
|
742
|
-
|
|
745
|
+
this._paintRecordList(tmpRecordListData, pBodyOnly);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Paint the computed record-list data into the DOM.
|
|
750
|
+
*
|
|
751
|
+
* Full render (pBodyOnly falsy): render the whole `PRSP_Renderable_List` (title, header, filters,
|
|
752
|
+
* pagination, rows) into the list destination — the original behavior.
|
|
753
|
+
*
|
|
754
|
+
* Body-only render (pBodyOnly true): only the page changed, so re-render just the rows and the two
|
|
755
|
+
* pagination strips into their stable containers, leaving the filter view (and its picker/control state)
|
|
756
|
+
* completely untouched. Each child is rendered with the freshly-computed record passed as an object, so
|
|
757
|
+
* it produces exactly what the inline `{~V:~}` render would have.
|
|
758
|
+
*
|
|
759
|
+
* @param {Record<string, any>} pRecordListData - The fully-computed list data (records, pagination, cells).
|
|
760
|
+
* @param {boolean} [pBodyOnly] - When true, surgically re-render only rows + pagination.
|
|
761
|
+
* @return {void}
|
|
762
|
+
*/
|
|
763
|
+
_paintRecordList(pRecordListData, pBodyOnly)
|
|
764
|
+
{
|
|
765
|
+
const fLogRendered = function (pError)
|
|
766
|
+
{
|
|
767
|
+
if (pError)
|
|
743
768
|
{
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
769
|
+
this.pict.log.error(`RecordSetList: Error rendering list ${pError}`, pRecordListData);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (this.pict.LogNoisiness > 0)
|
|
773
|
+
{
|
|
774
|
+
this.pict.log.info(`RecordSetList: Rendered list ${pRecordListData.RecordSet} with ${pRecordListData.Records.Records.length} records.`, pRecordListData);
|
|
775
|
+
}
|
|
776
|
+
else
|
|
777
|
+
{
|
|
778
|
+
this.pict.log.info(`RecordSetList: Rendered list ${pRecordListData.RecordSet} with ${pRecordListData.Records.Records.length} records.`);
|
|
779
|
+
}
|
|
780
|
+
}.bind(this);
|
|
749
781
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
782
|
+
if (!pBodyOnly)
|
|
783
|
+
{
|
|
784
|
+
this.renderAsync('PRSP_Renderable_List', pRecordListData.RenderDestination, pRecordListData, fLogRendered);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Page-only change: re-render the two pagination strips (current page + "showing X of Y") and the
|
|
789
|
+
// rows, each into its own stable container. The filter view, title, and header list are left as-is.
|
|
790
|
+
this.childViews.paginationTop.renderAsync('PRSP_Renderable_PaginationTop', '#PRSP_PaginationTop_Container', pRecordListData, null, () => { });
|
|
791
|
+
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, fLogRendered);
|
|
759
793
|
}
|
|
760
794
|
|
|
761
795
|
onInitialize()
|