pict-section-recordset 1.10.0 → 1.11.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/package.json +2 -2
- package/source/providers/RecordSet-RecordProvider-Base.js +13 -1
- package/source/providers/RecordSet-RecordProvider-MeadowEndpoints.js +37 -13
- package/source/views/RecordSet-Filters.js +86 -2
- package/source/views/filters/RecordSet-Filter-DistinctSelectedValueList.js +233 -0
- package/source/views/filters/index.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-section-recordset",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
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.384",
|
|
41
41
|
"pict-application": "^1.0.34",
|
|
42
42
|
"pict-docuserve": "^1.4.19",
|
|
43
43
|
"pict-service-commandlineutility": "^1.0.19",
|
|
@@ -316,6 +316,12 @@ class RecordSetProviderBase extends libPictProvider
|
|
|
316
316
|
tmpClause = JSON.parse(JSON.stringify(tmpClause));
|
|
317
317
|
tmpClause.Hash = `${pFilterKey}-${pClauseKey}-${this.pict.getUUID()}`;
|
|
318
318
|
tmpClause.Label = tmpClause.Label || tmpClause.DisplayName;
|
|
319
|
+
// Stamp the owning recordset so per-clause re-renders (which rebuild the render record
|
|
320
|
+
// from the live clause alone) can still resolve their provider and remove control.
|
|
321
|
+
if (!tmpClause.RecordSet && this.options.RecordSet)
|
|
322
|
+
{
|
|
323
|
+
tmpClause.RecordSet = this.options.RecordSet;
|
|
324
|
+
}
|
|
319
325
|
const tmpClauses = this.getFilterClauses();
|
|
320
326
|
tmpClauses.push(tmpClause);
|
|
321
327
|
}
|
|
@@ -467,7 +473,8 @@ class RecordSetProviderBase extends libPictProvider
|
|
|
467
473
|
*/
|
|
468
474
|
_pickQuickClause(pAvailableClauses)
|
|
469
475
|
{
|
|
470
|
-
return pAvailableClauses.find((pClause) => pClause.Type === '
|
|
476
|
+
return pAvailableClauses.find((pClause) => pClause.Type === 'DistinctSelectedValueList')
|
|
477
|
+
|| pAvailableClauses.find((pClause) => pClause.Type === 'InternalJoinSelectedValue' || pClause.Type === 'InternalJoinSelectedValueList')
|
|
471
478
|
|| pAvailableClauses.find((pClause) => pClause.Type === 'StringMatch' && pClause.ExactMatch === false)
|
|
472
479
|
|| pAvailableClauses.find((pClause) => pClause.Type === 'DateRange')
|
|
473
480
|
|| pAvailableClauses.find((pClause) => pClause.Type === 'NumericRange')
|
|
@@ -484,6 +491,7 @@ class RecordSetProviderBase extends libPictProvider
|
|
|
484
491
|
{
|
|
485
492
|
case 'StringMatch': return 'text';
|
|
486
493
|
case 'DateRange': return 'daterange';
|
|
494
|
+
case 'DistinctSelectedValueList': return 'distinct';
|
|
487
495
|
case 'InternalJoinSelectedValue':
|
|
488
496
|
case 'InternalJoinSelectedValueList': return 'entity';
|
|
489
497
|
default: return null;
|
|
@@ -668,6 +676,10 @@ class RecordSetProviderBase extends libPictProvider
|
|
|
668
676
|
tmpClause.Label = tmpClause.Label || tmpClause.DisplayName;
|
|
669
677
|
tmpClause.QuickFilter = true;
|
|
670
678
|
tmpClause.QuickFilterKey = pQuickFilterKey;
|
|
679
|
+
if (!tmpClause.RecordSet && this.options.RecordSet)
|
|
680
|
+
{
|
|
681
|
+
tmpClause.RecordSet = this.options.RecordSet;
|
|
682
|
+
}
|
|
671
683
|
return tmpClause;
|
|
672
684
|
}
|
|
673
685
|
|
|
@@ -68,35 +68,50 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
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
|
|
73
|
-
*
|
|
71
|
+
* Meadow's `<Entity>s/Distinct/<Column>` endpoint. Drives the `ScopeToRecordSet` filter knob
|
|
72
|
+
* (an entity picker limited to `FBL~<Column>~INN~<these values>`) and the
|
|
73
|
+
* `DistinctSelectedValueList` filter type (a dropdown whose options ARE these values).
|
|
74
|
+
* Cached per column; the cache is cleared on create/update/delete through this provider.
|
|
74
75
|
*
|
|
75
|
-
* @param {string} pColumn
|
|
76
|
+
* @param {string} pColumn
|
|
77
|
+
* @param {{ Filter?: string } | ((pError: Error|null, pValues: Array<any>) => void)} [pOptions] - Optional; `Filter` is a FoxHound stanza appended as `/FilteredTo/<Filter>` on the distinct query.
|
|
78
|
+
* @param {(pError: Error|null, pValues: Array<any>) => void} [fCallback]
|
|
76
79
|
*/
|
|
77
|
-
getRecordSetColumnDistinct(pColumn, fCallback)
|
|
80
|
+
getRecordSetColumnDistinct(pColumn, pOptions, fCallback)
|
|
78
81
|
{
|
|
82
|
+
// Back-compat: (pColumn, fCallback)
|
|
83
|
+
let tmpOptions = pOptions;
|
|
84
|
+
let tmpCallback = fCallback;
|
|
85
|
+
if (typeof tmpOptions === 'function')
|
|
86
|
+
{
|
|
87
|
+
tmpCallback = tmpOptions;
|
|
88
|
+
tmpOptions = {};
|
|
89
|
+
}
|
|
90
|
+
tmpOptions = tmpOptions || {};
|
|
91
|
+
// Unfiltered fetches keep the bare-column key (synchronous peeks elsewhere read it);
|
|
92
|
+
// filtered fetches get their own key so the two never cross-pollinate.
|
|
93
|
+
const tmpCacheKey = tmpOptions.Filter ? `${pColumn}::${tmpOptions.Filter}` : pColumn;
|
|
79
94
|
this._scopeDistinctCache = this._scopeDistinctCache || {};
|
|
80
|
-
if (Array.isArray(this._scopeDistinctCache[
|
|
95
|
+
if (Array.isArray(this._scopeDistinctCache[tmpCacheKey]))
|
|
81
96
|
{
|
|
82
|
-
return
|
|
97
|
+
return tmpCallback(null, this._scopeDistinctCache[tmpCacheKey]);
|
|
83
98
|
}
|
|
84
99
|
if (!this.options.Entity || !this.entityProvider || !this.entityProvider.restClient)
|
|
85
100
|
{
|
|
86
|
-
return
|
|
101
|
+
return tmpCallback(new Error('RecordSet provider cannot resolve a distinct request (missing Entity or rest client).'), []);
|
|
87
102
|
}
|
|
88
|
-
const tmpURL = `${this.options.URLPrefix || ''}${this.options.Entity}s/Distinct/${pColumn}`;
|
|
103
|
+
const tmpURL = `${this.options.URLPrefix || ''}${this.options.Entity}s/Distinct/${pColumn}${tmpOptions.Filter ? `/FilteredTo/${tmpOptions.Filter}` : ''}`;
|
|
89
104
|
this.entityProvider.restClient.getJSON(tmpURL, (pError, pResponse, pBody) =>
|
|
90
105
|
{
|
|
91
106
|
if (pError || (pResponse && pResponse.statusCode > 299) || !Array.isArray(pBody))
|
|
92
107
|
{
|
|
93
108
|
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[
|
|
95
|
-
return
|
|
109
|
+
this._scopeDistinctCache[tmpCacheKey] = [];
|
|
110
|
+
return tmpCallback(pError || new Error('distinct fetch returned a non-array'), []);
|
|
96
111
|
}
|
|
97
112
|
const tmpValues = [ ...new Set(pBody.map((pRecord) => pRecord && pRecord[pColumn]).filter((pValue) => pValue != null)) ];
|
|
98
|
-
this._scopeDistinctCache[
|
|
99
|
-
return
|
|
113
|
+
this._scopeDistinctCache[tmpCacheKey] = tmpValues;
|
|
114
|
+
return tmpCallback(null, tmpValues);
|
|
100
115
|
});
|
|
101
116
|
}
|
|
102
117
|
|
|
@@ -547,6 +562,9 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
547
562
|
}
|
|
548
563
|
// A new record changes the total; drop the cached count so the next render re-counts.
|
|
549
564
|
this._RecordSetCountCache = null;
|
|
565
|
+
// Mutations can introduce/retire column values; drop the distinct cache so
|
|
566
|
+
// ScopeToRecordSet scoping and DistinctSelectedValueList dropdowns refresh.
|
|
567
|
+
this._scopeDistinctCache = null;
|
|
550
568
|
// Drop this list's scoped cache too, so the next render re-fetches fresh.
|
|
551
569
|
if (typeof this.pict.EntityProvider.clearScope === 'function')
|
|
552
570
|
{
|
|
@@ -581,6 +599,9 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
581
599
|
}
|
|
582
600
|
// An edit can move a record in or out of the active filter; drop the cached count to be safe.
|
|
583
601
|
this._RecordSetCountCache = null;
|
|
602
|
+
// Mutations can introduce/retire column values; drop the distinct cache so
|
|
603
|
+
// ScopeToRecordSet scoping and DistinctSelectedValueList dropdowns refresh.
|
|
604
|
+
this._scopeDistinctCache = null;
|
|
584
605
|
// Drop this list's scoped cache too, so the next render re-fetches fresh.
|
|
585
606
|
if (typeof this.pict.EntityProvider.clearScope === 'function')
|
|
586
607
|
{
|
|
@@ -615,6 +636,9 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
615
636
|
}
|
|
616
637
|
// A delete changes the total; drop the cached count so the next render re-counts.
|
|
617
638
|
this._RecordSetCountCache = null;
|
|
639
|
+
// Mutations can introduce/retire column values; drop the distinct cache so
|
|
640
|
+
// ScopeToRecordSet scoping and DistinctSelectedValueList dropdowns refresh.
|
|
641
|
+
this._scopeDistinctCache = null;
|
|
618
642
|
// Drop this list's scoped cache too, so the next render re-fetches fresh.
|
|
619
643
|
if (typeof this.pict.EntityProvider.clearScope === 'function')
|
|
620
644
|
{
|
|
@@ -293,6 +293,7 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
|
|
|
293
293
|
{~TS:PRSP-QuickFilter-Text:Record.TextSlot~}
|
|
294
294
|
{~TS:PRSP-QuickFilter-Date:Record.DateSlot~}
|
|
295
295
|
{~TS:PRSP-QuickFilter-Entity:Record.EntitySlot~}
|
|
296
|
+
{~TS:PRSP-QuickFilter-Distinct:Record.DistinctSlot~}
|
|
296
297
|
</div>
|
|
297
298
|
`
|
|
298
299
|
},
|
|
@@ -325,6 +326,14 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
|
|
|
325
326
|
Hash: 'PRSP-QuickFilter-Entity',
|
|
326
327
|
Template: /*html*/`
|
|
327
328
|
<span class="prsp-quickfilter-entityhost" id="{~D:Record.HostID~}"></span>
|
|
329
|
+
`
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
// Distinct-values control — a multi-select pict-section-picker mounts into this host
|
|
333
|
+
// (post-render, in _mountQuickFilterDistinct), its options the column's distinct values.
|
|
334
|
+
Hash: 'PRSP-QuickFilter-Distinct',
|
|
335
|
+
Template: /*html*/`
|
|
336
|
+
<span class="prsp-quickfilter-entityhost" id="{~D:Record.HostID~}"></span>
|
|
328
337
|
`
|
|
329
338
|
},
|
|
330
339
|
],
|
|
@@ -637,10 +646,11 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
637
646
|
// `quickFiltersAutoDefault` (host-settable, default on) gates the clever schema defaults: a host
|
|
638
647
|
// can set it false to make quick filters opt-in (only record sets with an explicit config show).
|
|
639
648
|
const tmpEntityMounts = [];
|
|
649
|
+
const tmpDistinctMounts = [];
|
|
640
650
|
const tmpItems = tmpProvider.getQuickFilterDefinitions(this.quickFiltersAutoDefault).map((pDefinition) =>
|
|
641
651
|
{
|
|
642
652
|
const tmpBase = { Field: pDefinition.Field, ClauseKey: pDefinition.ClauseKey, Label: pDefinition.Label, RecordSet: pRecordSet, ViewContext: pViewContext };
|
|
643
|
-
const tmpItem = { Label: pDefinition.Label, TextSlot: [], DateSlot: [], EntitySlot: [] };
|
|
653
|
+
const tmpItem = { Label: pDefinition.Label, TextSlot: [], DateSlot: [], EntitySlot: [], DistinctSlot: [] };
|
|
644
654
|
if (pDefinition.Control === 'text')
|
|
645
655
|
{
|
|
646
656
|
tmpItem.TextSlot = [ Object.assign({}, tmpBase, { Value: tmpProvider.getQuickFilterClauseValue(pDefinition.Field), Placeholder: `Search ${pDefinition.Label}…` }) ];
|
|
@@ -656,12 +666,19 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
656
666
|
tmpItem.EntitySlot = [ Object.assign({}, tmpBase, { HostID: tmpHostID }) ];
|
|
657
667
|
tmpEntityMounts.push(Object.assign({}, tmpBase, { HostID: tmpHostID }));
|
|
658
668
|
}
|
|
669
|
+
else if (pDefinition.Control === 'distinct')
|
|
670
|
+
{
|
|
671
|
+
const tmpHostID = `PRSP_QuickDistinct_${pRecordSet}_${pDefinition.Field}`;
|
|
672
|
+
tmpItem.DistinctSlot = [ Object.assign({}, tmpBase, { HostID: tmpHostID }) ];
|
|
673
|
+
tmpDistinctMounts.push(Object.assign({}, tmpBase, { HostID: tmpHostID }));
|
|
674
|
+
}
|
|
659
675
|
return tmpItem;
|
|
660
676
|
});
|
|
661
677
|
const tmpHTML = (tmpItems.length > 0) ? this.pict.parseTemplateByHash('PRSP-QuickFilters-Bar', { Filters: tmpItems }) : '';
|
|
662
678
|
this.pict.ContentAssignment.assignContent('#PRSP_QuickFilters', tmpHTML);
|
|
663
|
-
// Entity controls: mount (or re-mount) a picker into each host after the wholesale re-render.
|
|
679
|
+
// Entity / distinct controls: mount (or re-mount) a picker into each host after the wholesale re-render.
|
|
664
680
|
tmpEntityMounts.forEach((pMount) => this._mountQuickFilterEntity(pRecordSet, pViewContext, pMount));
|
|
681
|
+
tmpDistinctMounts.forEach((pMount) => this._mountQuickFilterDistinct(pRecordSet, pViewContext, pMount));
|
|
665
682
|
}
|
|
666
683
|
|
|
667
684
|
/**
|
|
@@ -720,6 +737,56 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
720
737
|
tmpView.setValue((Array.isArray(tmpCurrent) && tmpCurrent.length > 0) ? tmpCurrent[0] : '');
|
|
721
738
|
}
|
|
722
739
|
|
|
740
|
+
/**
|
|
741
|
+
* Mount (idempotently) a multi-select pict-section-picker into a quick-filter distinct host,
|
|
742
|
+
* its options the clause's static `Options` or the column's distinct values (fetched + cached
|
|
743
|
+
* on the recordset provider; re-mounted once the fetch resolves so the options fill in). The
|
|
744
|
+
* picker substring-filters static options client-side, so search works without a server trip.
|
|
745
|
+
* On change it STAGES the clause (Values array) — Apply / Search commits. No-op if the picker
|
|
746
|
+
* module isn't registered.
|
|
747
|
+
*
|
|
748
|
+
* @param {string} pRecordSet @param {string} pViewContext @param {Record<string, any>} pMount
|
|
749
|
+
*/
|
|
750
|
+
_mountQuickFilterDistinct(pRecordSet, pViewContext, pMount)
|
|
751
|
+
{
|
|
752
|
+
if (!document.getElementById(pMount.HostID)) { return; }
|
|
753
|
+
const tmpPickerProvider = this.pict.providers['Pict-Section-Picker'];
|
|
754
|
+
const tmpProvider = this.pict.providers['RSP-Provider-' + pRecordSet];
|
|
755
|
+
if (!tmpPickerProvider || typeof tmpPickerProvider.createPicker !== 'function' || !tmpProvider) { return; }
|
|
756
|
+
const tmpDescriptor = tmpProvider.getFilterClauseSchemaForKey(pMount.Field)?.AvailableClauses?.find?.((pClause) => pClause.ClauseKey === pMount.ClauseKey);
|
|
757
|
+
if (!tmpDescriptor || !tmpDescriptor.FilterByColumn) { return; }
|
|
758
|
+
let tmpValues;
|
|
759
|
+
if (Array.isArray(tmpDescriptor.Options) && tmpDescriptor.Options.length > 0)
|
|
760
|
+
{
|
|
761
|
+
tmpValues = tmpDescriptor.Options;
|
|
762
|
+
}
|
|
763
|
+
else
|
|
764
|
+
{
|
|
765
|
+
const tmpCacheKey = tmpDescriptor.DistinctFilter ? `${tmpDescriptor.FilterByColumn}::${tmpDescriptor.DistinctFilter}` : tmpDescriptor.FilterByColumn;
|
|
766
|
+
tmpValues = (tmpProvider._scopeDistinctCache || {})[tmpCacheKey];
|
|
767
|
+
if (!Array.isArray(tmpValues) && typeof tmpProvider.getRecordSetColumnDistinct === 'function')
|
|
768
|
+
{
|
|
769
|
+
tmpProvider.getRecordSetColumnDistinct(tmpDescriptor.FilterByColumn, { Filter: tmpDescriptor.DistinctFilter },
|
|
770
|
+
() => this._mountQuickFilterDistinct(pRecordSet, pViewContext, pMount));
|
|
771
|
+
tmpValues = [];
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const tmpOptions = (Array.isArray(tmpValues) ? tmpValues : []).map((pValue) => ({ Value: pValue, Text: String(pValue) }));
|
|
775
|
+
const tmpView = tmpPickerProvider.createPicker(`Quick-Distinct-${pRecordSet}-${pMount.Field}`,
|
|
776
|
+
{
|
|
777
|
+
DestinationAddress: `#${pMount.HostID}`,
|
|
778
|
+
// Multi-select: "any of the checked values" is the natural distinct-filter semantic.
|
|
779
|
+
Mode: 'multi',
|
|
780
|
+
Options: tmpOptions,
|
|
781
|
+
Placeholder: `Select ${pMount.Label}…`,
|
|
782
|
+
OnChange: (pValuesArray) => this.applyQuickFilterDistinct(pRecordSet, pViewContext, pMount.Field, pMount.ClauseKey, pValuesArray),
|
|
783
|
+
});
|
|
784
|
+
if (!tmpView) { return; }
|
|
785
|
+
tmpView.render();
|
|
786
|
+
const tmpCurrent = (typeof tmpProvider.getQuickFilterEntityValue === 'function') ? tmpProvider.getQuickFilterEntityValue(pMount.Field) : [];
|
|
787
|
+
tmpView.setValue(Array.isArray(tmpCurrent) ? tmpCurrent : []);
|
|
788
|
+
}
|
|
789
|
+
|
|
723
790
|
/**
|
|
724
791
|
* Apply a text quick filter: upsert (or clear) its tagged clause, then run the standard search +
|
|
725
792
|
* serialize path. Commits on blur / Enter (not per-keystroke) so the re-render never steals focus.
|
|
@@ -820,6 +887,23 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
820
887
|
}
|
|
821
888
|
}
|
|
822
889
|
|
|
890
|
+
/**
|
|
891
|
+
* Stage a field's distinct quick-filter selection (the Values array of its
|
|
892
|
+
* DistinctSelectedValueList clause). Doesn't fire the search — commit happens
|
|
893
|
+
* on Apply / Search.
|
|
894
|
+
*
|
|
895
|
+
* @param {string} pRecordSet @param {string} pViewContext @param {string} pField @param {string} pClauseKey @param {Array<any>} pValues
|
|
896
|
+
*/
|
|
897
|
+
applyQuickFilterDistinct(pRecordSet, pViewContext, pField, pClauseKey, pValues)
|
|
898
|
+
{
|
|
899
|
+
this.bumpRenderEpoch();
|
|
900
|
+
const tmpProvider = this.pict.providers['RSP-Provider-' + pRecordSet];
|
|
901
|
+
if (tmpProvider && typeof tmpProvider.upsertQuickFilterEntity === 'function')
|
|
902
|
+
{
|
|
903
|
+
tmpProvider.upsertQuickFilterEntity(pField, pClauseKey, pValues);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
823
907
|
/**
|
|
824
908
|
* @param {Event} pEvent - The DOM event that triggered the search
|
|
825
909
|
* @param {string} pRecordSet - The record set being filtered
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const ViewRecordSetSUBSETFilterBase = require('./RecordSet-Filter-Base');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DistinctSelectedValueList — a dropdown/checkbox filter whose options ARE the distinct
|
|
5
|
+
* values of a column on the CORE entity (no join), fetched from Meadow's
|
|
6
|
+
* `<Entity>s/Distinct/<Column>` endpoint through the recordset provider's cached
|
|
7
|
+
* `getRecordSetColumnDistinct()`. The clause's `Values` array holds the selection; pict's
|
|
8
|
+
* Filter.js compiles it to an OR-chain of `FBVOR~<col>~EQ~<value>` stanzas inside one
|
|
9
|
+
* paren group.
|
|
10
|
+
*
|
|
11
|
+
* Clause config:
|
|
12
|
+
* - FilterByColumn {string} - the core-entity column the options come from / filter on.
|
|
13
|
+
* - Options {Array<any>} - optional STATIC option list; when present the distinct fetch is skipped.
|
|
14
|
+
* - DistinctFilter {string} - optional FoxHound stanza appended as `/FilteredTo/<...>` on the
|
|
15
|
+
* distinct query (e.g. `FBV~Deleted~EQ~0` to keep soft-deleted rows' values out of the list).
|
|
16
|
+
*
|
|
17
|
+
* Selected values missing from the option list (e.g. restored from a saved filter experience
|
|
18
|
+
* whose value no longer appears in the data) are unioned in so they stay visible and
|
|
19
|
+
* un-checkable. Selection toggles by option INDEX, never by value — values can contain
|
|
20
|
+
* quotes that would break inline onclick attributes.
|
|
21
|
+
*
|
|
22
|
+
* Staging only: toggling checkboxes mutates the live clause but does NOT fire a search —
|
|
23
|
+
* the user commits with Apply / Search (the stage-then-Apply contract).
|
|
24
|
+
*/
|
|
25
|
+
const _DEFAULT_CONFIGURATION_Filter_DistinctSelectedValueList =
|
|
26
|
+
{
|
|
27
|
+
ViewIdentifier: 'PRSP-FilterType-DistinctSelectedValueList',
|
|
28
|
+
|
|
29
|
+
CSS: /*css*/`
|
|
30
|
+
.prsp-filter-distinct { min-width: 12rem; }
|
|
31
|
+
.prsp-filter-distinct-options { display: flex; flex-direction: column; gap: 0.15rem; max-height: 11rem; overflow-y: auto;
|
|
32
|
+
padding: 0.35rem 0.5rem; border-radius: 8px; border: 1px solid var(--theme-color-border-default, #d7dce3);
|
|
33
|
+
background: var(--theme-color-background-primary, #fff); }
|
|
34
|
+
.prsp-filter-distinct-option { display: flex; align-items: center; gap: 0.45rem; cursor: pointer;
|
|
35
|
+
font-size: 0.92rem; color: var(--theme-color-text-primary, #1f2733); margin: 0; }
|
|
36
|
+
.prsp-filter-distinct-option input[type="checkbox"] { width: auto; margin: 0; cursor: pointer; }
|
|
37
|
+
.prsp-filter-distinct-note { font-size: 0.82rem; color: var(--theme-color-text-muted, #6b7686); padding: 0.15rem 0; }
|
|
38
|
+
`,
|
|
39
|
+
|
|
40
|
+
Templates:
|
|
41
|
+
[
|
|
42
|
+
{
|
|
43
|
+
Hash: 'PRSP-Filter-DistinctSelectedValueList-Template',
|
|
44
|
+
Template: /*html*/`
|
|
45
|
+
<!-- DefaultPackage pict view template: [PRSP-Filter-DistinctSelectedValueList-Template] -->
|
|
46
|
+
<div class="prsp-filter-distinct">
|
|
47
|
+
<label>{~D:Record.Label~}</label>
|
|
48
|
+
<div class="prsp-filter-distinct-options">
|
|
49
|
+
{~TS:PRSP-Filter-Distinct-Loading:Record.DistinctLoading~}
|
|
50
|
+
{~TS:PRSP-Filter-Distinct-Empty:Record.DistinctEmpty~}
|
|
51
|
+
{~TSWP:PRSP-Filter-Distinct-Option:Record.DistinctOptions:Record~}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<!-- DefaultPackage end view template: [PRSP-Filter-DistinctSelectedValueList-Template] -->
|
|
55
|
+
`
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
Hash: 'PRSP-Filter-Distinct-Option',
|
|
59
|
+
Template: /*html*/`
|
|
60
|
+
<label class="prsp-filter-distinct-option">
|
|
61
|
+
<input type="checkbox" {~D:Record.Data.CheckedAttribute~}
|
|
62
|
+
onchange="_Pict.views['PRSP-FilterType-DistinctSelectedValueList'].toggleOption(event, '{~D:Record.Payload.ClauseAddress~}', '{~D:Record.Payload.Hash~}', {~D:Record.Data.OptionIndex~})">
|
|
63
|
+
<span>{~D:Record.Data.Text~}</span>
|
|
64
|
+
</label>
|
|
65
|
+
`
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
Hash: 'PRSP-Filter-Distinct-Loading',
|
|
69
|
+
Template: /*html*/`
|
|
70
|
+
<span class="prsp-filter-distinct-note">Loading values…</span>
|
|
71
|
+
`
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
Hash: 'PRSP-Filter-Distinct-Empty',
|
|
75
|
+
Template: /*html*/`
|
|
76
|
+
<span class="prsp-filter-distinct-note">No values available.</span>
|
|
77
|
+
`
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
class ViewRecordSetSUBSETFilterDistinctSelectedValueList extends ViewRecordSetSUBSETFilterBase
|
|
83
|
+
{
|
|
84
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
85
|
+
{
|
|
86
|
+
super(pFable, pOptions, pServiceHash);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getFilterFormTemplate()
|
|
90
|
+
{
|
|
91
|
+
return 'PRSP-Filter-DistinctSelectedValueList-Template';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The ordered raw option values for a clause: the static `Options` list when configured,
|
|
96
|
+
* else the provider's cached distinct values, with any selected values missing from the
|
|
97
|
+
* list unioned onto the end.
|
|
98
|
+
*
|
|
99
|
+
* @param {Record<string, any>} pClause
|
|
100
|
+
* @param {Record<string, any>} pProvider - The recordset provider (`RSP-Provider-<RecordSet>`).
|
|
101
|
+
* @return {Array<any>}
|
|
102
|
+
*/
|
|
103
|
+
_optionValues(pClause, pProvider)
|
|
104
|
+
{
|
|
105
|
+
let tmpValues = Array.isArray(pClause.Options) ? pClause.Options.slice() : null;
|
|
106
|
+
if (!tmpValues)
|
|
107
|
+
{
|
|
108
|
+
const tmpCacheKey = pClause.DistinctFilter ? `${pClause.FilterByColumn}::${pClause.DistinctFilter}` : pClause.FilterByColumn;
|
|
109
|
+
const tmpCached = (pProvider && pProvider._scopeDistinctCache) ? pProvider._scopeDistinctCache[tmpCacheKey] : null;
|
|
110
|
+
tmpValues = Array.isArray(tmpCached) ? tmpCached.slice() : [];
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(pClause.Values))
|
|
113
|
+
{
|
|
114
|
+
for (const tmpSelected of pClause.Values)
|
|
115
|
+
{
|
|
116
|
+
if (!tmpValues.some((pValue) => pValue === tmpSelected))
|
|
117
|
+
{
|
|
118
|
+
tmpValues.push(tmpSelected);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return tmpValues;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Map the option values to render rows ({ Text, OptionIndex, CheckedAttribute }).
|
|
127
|
+
*
|
|
128
|
+
* @param {Record<string, any>} pClause
|
|
129
|
+
* @param {Record<string, any>} pProvider
|
|
130
|
+
* @return {Array<{ Text: string, OptionIndex: number, CheckedAttribute: string }>}
|
|
131
|
+
*/
|
|
132
|
+
_composeOptionList(pClause, pProvider)
|
|
133
|
+
{
|
|
134
|
+
const tmpSelected = Array.isArray(pClause.Values) ? pClause.Values : [];
|
|
135
|
+
return this._optionValues(pClause, pProvider).map((pValue, pIndex) => (
|
|
136
|
+
{
|
|
137
|
+
Text: String(pValue),
|
|
138
|
+
OptionIndex: pIndex,
|
|
139
|
+
CheckedAttribute: tmpSelected.some((pSelectedValue) => pSelectedValue === pValue) ? 'checked' : '',
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {Record<string, any>} pRecord
|
|
145
|
+
*/
|
|
146
|
+
prepareRecord(pRecord)
|
|
147
|
+
{
|
|
148
|
+
super.prepareRecord(pRecord);
|
|
149
|
+
|
|
150
|
+
const tmpProvider = this.pict.providers['RSP-Provider-' + pRecord.RecordSet];
|
|
151
|
+
pRecord.DistinctLoading = [];
|
|
152
|
+
// No static Options: make sure the distinct values are fetched (cached on the provider),
|
|
153
|
+
// re-rendering just this clause's container once they resolve. Errored fetches cache []
|
|
154
|
+
// so this can't loop.
|
|
155
|
+
if (!Array.isArray(pRecord.Options) && pRecord.FilterByColumn
|
|
156
|
+
&& tmpProvider && typeof tmpProvider.getRecordSetColumnDistinct === 'function')
|
|
157
|
+
{
|
|
158
|
+
const tmpCacheKey = pRecord.DistinctFilter ? `${pRecord.FilterByColumn}::${pRecord.DistinctFilter}` : pRecord.FilterByColumn;
|
|
159
|
+
if (!tmpProvider._scopeDistinctCache || !Array.isArray(tmpProvider._scopeDistinctCache[tmpCacheKey]))
|
|
160
|
+
{
|
|
161
|
+
pRecord.DistinctLoading = [ {} ];
|
|
162
|
+
const tmpClauseAddress = pRecord.ClauseAddress;
|
|
163
|
+
const tmpClauseHash = pRecord.Hash;
|
|
164
|
+
tmpProvider.getRecordSetColumnDistinct(pRecord.FilterByColumn, { Filter: pRecord.DistinctFilter },
|
|
165
|
+
() => this._reRenderClause(tmpClauseAddress, tmpClauseHash));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
pRecord.DistinctOptions = this._composeOptionList(pRecord, tmpProvider);
|
|
169
|
+
pRecord.DistinctEmpty = (pRecord.DistinctLoading.length === 0 && pRecord.DistinctOptions.length === 0) ? [ {} ] : [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Toggle an option's membership in the live clause's `Values` by option index. Staging
|
|
174
|
+
* only — the search fires on Apply / Search, not here.
|
|
175
|
+
*
|
|
176
|
+
* @param {Event} pEvent
|
|
177
|
+
* @param {string} pClauseInformaryAddress
|
|
178
|
+
* @param {string} pClauseHash
|
|
179
|
+
* @param {number} pOptionIndex
|
|
180
|
+
*/
|
|
181
|
+
toggleOption(pEvent, pClauseInformaryAddress, pClauseHash, pOptionIndex)
|
|
182
|
+
{
|
|
183
|
+
const tmpClause = this.getInformaryScopedValue(pClauseInformaryAddress);
|
|
184
|
+
if (!tmpClause)
|
|
185
|
+
{
|
|
186
|
+
this.pict.log.error(`[Filter-DistinctSelectedValueList] No clause found for address: ${pClauseInformaryAddress}`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const tmpProvider = this.pict.providers['RSP-Provider-' + tmpClause.RecordSet];
|
|
190
|
+
const tmpOptionValues = this._optionValues(tmpClause, tmpProvider);
|
|
191
|
+
if (pOptionIndex < 0 || pOptionIndex >= tmpOptionValues.length)
|
|
192
|
+
{
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const tmpValue = tmpOptionValues[pOptionIndex];
|
|
196
|
+
if (!Array.isArray(tmpClause.Values))
|
|
197
|
+
{
|
|
198
|
+
tmpClause.Values = [];
|
|
199
|
+
}
|
|
200
|
+
const tmpSelectedIndex = tmpClause.Values.findIndex((pSelectedValue) => pSelectedValue === tmpValue);
|
|
201
|
+
if (tmpSelectedIndex >= 0)
|
|
202
|
+
{
|
|
203
|
+
tmpClause.Values.splice(tmpSelectedIndex, 1);
|
|
204
|
+
}
|
|
205
|
+
else
|
|
206
|
+
{
|
|
207
|
+
tmpClause.Values.push(tmpValue);
|
|
208
|
+
}
|
|
209
|
+
this._reRenderClause(pClauseInformaryAddress, pClauseHash);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Re-render this clause's container (the checkbox list repaints on toggle / fetch resolve).
|
|
214
|
+
*
|
|
215
|
+
* @param {string} pClauseInformaryAddress
|
|
216
|
+
* @param {string} pClauseHash
|
|
217
|
+
*/
|
|
218
|
+
_reRenderClause(pClauseInformaryAddress, pClauseHash)
|
|
219
|
+
{
|
|
220
|
+
const tmpClause = this.getInformaryScopedValue(pClauseInformaryAddress);
|
|
221
|
+
if (!tmpClause)
|
|
222
|
+
{
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const tmpRecord = Object.assign({ ClauseAddress: pClauseInformaryAddress }, tmpClause);
|
|
226
|
+
this.prepareRecord(tmpRecord);
|
|
227
|
+
this.render(null, `#PRSP_Filter_Container_${pClauseHash}`, tmpRecord);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = ViewRecordSetSUBSETFilterDistinctSelectedValueList;
|
|
232
|
+
|
|
233
|
+
module.exports.default_configuration = Object.assign({}, ViewRecordSetSUBSETFilterBase.default_configuration, _DEFAULT_CONFIGURATION_Filter_DistinctSelectedValueList);
|
|
@@ -10,6 +10,8 @@ module.exports =
|
|
|
10
10
|
StringMatch: require('./RecordSet-Filter-StringMatch.js'),
|
|
11
11
|
StringRange: require('./RecordSet-Filter-StringRange.js'),
|
|
12
12
|
|
|
13
|
+
DistinctSelectedValueList: require('./RecordSet-Filter-DistinctSelectedValueList.js'),
|
|
14
|
+
|
|
13
15
|
InternalJoinDateMatch: require('./RecordSet-Filter-InternalJoinDateMatch.js'),
|
|
14
16
|
InternalJoinDateRange: require('./RecordSet-Filter-InternalJoinDateRange.js'),
|
|
15
17
|
InternalJoinNumericMatch: require('./RecordSet-Filter-InternalJoinNumericMatch.js'),
|