pict-section-recordset 1.10.0 → 1.11.1

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.10.0",
3
+ "version": "1.11.1",
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.374",
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 === 'InternalJoinSelectedValue' || pClause.Type === 'InternalJoinSelectedValueList')
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 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.
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 @param {(pError: Error|null, pValues: Array<any>) => void} fCallback
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[pColumn]))
95
+ if (Array.isArray(this._scopeDistinctCache[tmpCacheKey]))
81
96
  {
82
- return fCallback(null, this._scopeDistinctCache[pColumn]);
97
+ return tmpCallback(null, this._scopeDistinctCache[tmpCacheKey]);
83
98
  }
84
99
  if (!this.options.Entity || !this.entityProvider || !this.entityProvider.restClient)
85
100
  {
86
- return fCallback(new Error('RecordSet provider cannot resolve a distinct request (missing Entity or rest client).'), []);
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[pColumn] = [];
95
- return fCallback(pError || new Error('distinct fetch returned a non-array'), []);
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[pColumn] = tmpValues;
99
- return fCallback(null, tmpValues);
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
  /**
@@ -711,6 +728,10 @@ class ViewRecordSetSUBSETFilters extends libPictView
711
728
  // TextField when the entity has no list-entry template.
712
729
  TextTemplate: tmpDescriptor.EntityListEntryTemplate || undefined,
713
730
  Placeholder: `Select ${pMount.Label}…`,
731
+ // A filter's natural zero state is "Any" — the pinned clear row / inline × empty the
732
+ // selection, OnChange(null) flows through the upsert, and the clause is removed.
733
+ AllowClear: true,
734
+ ClearLabel: 'Any',
714
735
  OnChange: (pValue) => this.applyQuickFilterEntity(pRecordSet, pViewContext, pMount.Field, pMount.ClauseKey, pValue),
715
736
  BaseFilter: tmpScopeBaseFilter,
716
737
  });
@@ -720,6 +741,56 @@ class ViewRecordSetSUBSETFilters extends libPictView
720
741
  tmpView.setValue((Array.isArray(tmpCurrent) && tmpCurrent.length > 0) ? tmpCurrent[0] : '');
721
742
  }
722
743
 
744
+ /**
745
+ * Mount (idempotently) a multi-select pict-section-picker into a quick-filter distinct host,
746
+ * its options the clause's static `Options` or the column's distinct values (fetched + cached
747
+ * on the recordset provider; re-mounted once the fetch resolves so the options fill in). The
748
+ * picker substring-filters static options client-side, so search works without a server trip.
749
+ * On change it STAGES the clause (Values array) — Apply / Search commits. No-op if the picker
750
+ * module isn't registered.
751
+ *
752
+ * @param {string} pRecordSet @param {string} pViewContext @param {Record<string, any>} pMount
753
+ */
754
+ _mountQuickFilterDistinct(pRecordSet, pViewContext, pMount)
755
+ {
756
+ if (!document.getElementById(pMount.HostID)) { return; }
757
+ const tmpPickerProvider = this.pict.providers['Pict-Section-Picker'];
758
+ const tmpProvider = this.pict.providers['RSP-Provider-' + pRecordSet];
759
+ if (!tmpPickerProvider || typeof tmpPickerProvider.createPicker !== 'function' || !tmpProvider) { return; }
760
+ const tmpDescriptor = tmpProvider.getFilterClauseSchemaForKey(pMount.Field)?.AvailableClauses?.find?.((pClause) => pClause.ClauseKey === pMount.ClauseKey);
761
+ if (!tmpDescriptor || !tmpDescriptor.FilterByColumn) { return; }
762
+ let tmpValues;
763
+ if (Array.isArray(tmpDescriptor.Options) && tmpDescriptor.Options.length > 0)
764
+ {
765
+ tmpValues = tmpDescriptor.Options;
766
+ }
767
+ else
768
+ {
769
+ const tmpCacheKey = tmpDescriptor.DistinctFilter ? `${tmpDescriptor.FilterByColumn}::${tmpDescriptor.DistinctFilter}` : tmpDescriptor.FilterByColumn;
770
+ tmpValues = (tmpProvider._scopeDistinctCache || {})[tmpCacheKey];
771
+ if (!Array.isArray(tmpValues) && typeof tmpProvider.getRecordSetColumnDistinct === 'function')
772
+ {
773
+ tmpProvider.getRecordSetColumnDistinct(tmpDescriptor.FilterByColumn, { Filter: tmpDescriptor.DistinctFilter },
774
+ () => this._mountQuickFilterDistinct(pRecordSet, pViewContext, pMount));
775
+ tmpValues = [];
776
+ }
777
+ }
778
+ const tmpOptions = (Array.isArray(tmpValues) ? tmpValues : []).map((pValue) => ({ Value: pValue, Text: String(pValue) }));
779
+ const tmpView = tmpPickerProvider.createPicker(`Quick-Distinct-${pRecordSet}-${pMount.Field}`,
780
+ {
781
+ DestinationAddress: `#${pMount.HostID}`,
782
+ // Multi-select: "any of the checked values" is the natural distinct-filter semantic.
783
+ Mode: 'multi',
784
+ Options: tmpOptions,
785
+ Placeholder: `Select ${pMount.Label}…`,
786
+ OnChange: (pValuesArray) => this.applyQuickFilterDistinct(pRecordSet, pViewContext, pMount.Field, pMount.ClauseKey, pValuesArray),
787
+ });
788
+ if (!tmpView) { return; }
789
+ tmpView.render();
790
+ const tmpCurrent = (typeof tmpProvider.getQuickFilterEntityValue === 'function') ? tmpProvider.getQuickFilterEntityValue(pMount.Field) : [];
791
+ tmpView.setValue(Array.isArray(tmpCurrent) ? tmpCurrent : []);
792
+ }
793
+
723
794
  /**
724
795
  * Apply a text quick filter: upsert (or clear) its tagged clause, then run the standard search +
725
796
  * serialize path. Commits on blur / Enter (not per-keystroke) so the re-render never steals focus.
@@ -820,6 +891,23 @@ class ViewRecordSetSUBSETFilters extends libPictView
820
891
  }
821
892
  }
822
893
 
894
+ /**
895
+ * Stage a field's distinct quick-filter selection (the Values array of its
896
+ * DistinctSelectedValueList clause). Doesn't fire the search — commit happens
897
+ * on Apply / Search.
898
+ *
899
+ * @param {string} pRecordSet @param {string} pViewContext @param {string} pField @param {string} pClauseKey @param {Array<any>} pValues
900
+ */
901
+ applyQuickFilterDistinct(pRecordSet, pViewContext, pField, pClauseKey, pValues)
902
+ {
903
+ this.bumpRenderEpoch();
904
+ const tmpProvider = this.pict.providers['RSP-Provider-' + pRecordSet];
905
+ if (tmpProvider && typeof tmpProvider.upsertQuickFilterEntity === 'function')
906
+ {
907
+ tmpProvider.upsertQuickFilterEntity(pField, pClauseKey, pValues);
908
+ }
909
+ }
910
+
823
911
  /**
824
912
  * @param {Event} pEvent - The DOM event that triggered the search
825
913
  * @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'),