pict-section-recordset 1.9.6 → 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.
Files changed (40) hide show
  1. package/README.md +38 -0
  2. package/package.json +2 -2
  3. package/source/Pict-Section-RecordSet.js +1 -0
  4. package/source/providers/Column-Data-Provider.js +219 -0
  5. package/source/providers/RecordSet-RecordProvider-Base.js +64 -1
  6. package/source/providers/RecordSet-RecordProvider-MeadowEndpoints.js +92 -16
  7. package/source/services/RecordsSet-MetaController.js +23 -1
  8. package/source/templates/Pict-Template-FilterInstanceViews.js +4 -0
  9. package/source/views/RecordSet-Filters.js +140 -3
  10. package/source/views/filters/RecordSet-Filter-DistinctSelectedValueList.js +233 -0
  11. package/source/views/filters/index.js +2 -0
  12. package/source/views/list/RecordSet-List-ColumnChooser.js +345 -0
  13. package/source/views/list/RecordSet-List-RecordListEntry.js +4 -1
  14. package/source/views/list/RecordSet-List.js +390 -15
  15. package/source/views/read/RecordSet-Read.js +65 -6
  16. package/types/Pict-Section-RecordSet.d.ts +1 -0
  17. package/types/providers/Column-Data-Provider.d.ts +115 -0
  18. package/types/providers/Column-Data-Provider.d.ts.map +1 -0
  19. package/types/providers/RecordSet-DynamicRecordsetSolver.d.ts +3 -0
  20. package/types/providers/RecordSet-DynamicRecordsetSolver.d.ts.map +1 -1
  21. package/types/providers/RecordSet-RecordProvider-Base.d.ts +110 -0
  22. package/types/providers/RecordSet-RecordProvider-Base.d.ts.map +1 -1
  23. package/types/providers/RecordSet-RecordProvider-MeadowEndpoints.d.ts +51 -1
  24. package/types/providers/RecordSet-RecordProvider-MeadowEndpoints.d.ts.map +1 -1
  25. package/types/providers/RecordSet-Router.d.ts +1 -0
  26. package/types/providers/RecordSet-Router.d.ts.map +1 -1
  27. package/types/services/RecordsSet-MetaController.d.ts.map +1 -1
  28. package/types/templates/Pict-Template-FilterInstanceViews.d.ts.map +1 -1
  29. package/types/views/RecordSet-Filters.d.ts +61 -0
  30. package/types/views/RecordSet-Filters.d.ts.map +1 -1
  31. package/types/views/RecordSet-RecordBaseView.d.ts +1 -0
  32. package/types/views/RecordSet-RecordBaseView.d.ts.map +1 -1
  33. package/types/views/filters/RecordSet-Filter-EntityReference-Base.d.ts.map +1 -1
  34. package/types/views/list/RecordSet-List-ColumnChooser.d.ts +68 -0
  35. package/types/views/list/RecordSet-List-ColumnChooser.d.ts.map +1 -0
  36. package/types/views/list/RecordSet-List-RecordListEntry.d.ts.map +1 -1
  37. package/types/views/list/RecordSet-List.d.ts +167 -2
  38. package/types/views/list/RecordSet-List.d.ts.map +1 -1
  39. package/types/views/read/RecordSet-Read.d.ts +8 -0
  40. package/types/views/read/RecordSet-Read.d.ts.map +1 -1
@@ -88,6 +88,12 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
88
88
  .prsp-quickfilter-dash { color: var(--theme-color-text-muted, #6b7686); }
89
89
  /* Entity quick control: the pict-section-picker mounts into this host (its own .pps chrome themes it). */
90
90
  .prsp-quickfilter-entityhost { display: inline-block; width: 14rem; max-width: 100%; vertical-align: middle; }
91
+ /* Show-deleted switch (RecordSetListShowDeletedFilter): a labeled checkbox in the drawer actions row.
92
+ Painted post-render into the host label; :empty hides it for record sets that haven't opted in. */
93
+ .prsp-filters-showdeleted { display: inline-flex; align-items: center; gap: 0.4rem; cursor: pointer; user-select: none;
94
+ font-size: 0.88rem; color: var(--theme-color-text-secondary, #45505f); margin-right: 0.35rem; }
95
+ .prsp-filters-showdeleted:empty { display: none; }
96
+ .prsp-filters-showdeleted-checkbox { width: 1.05rem; height: 1.05rem; cursor: pointer; accent-color: var(--theme-color-brand-primary, #156dd1); }
91
97
 
92
98
  /* Module-owned "Add filter" popover (replaces the old native <select> pickers). */
93
99
  /* Fixed (viewport-anchored) + JS-positioned on open, so no ancestor overflow:hidden — the filter card,
@@ -196,6 +202,7 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
196
202
  Template: /*html*/`
197
203
  <!-- DefaultPackage pict view template: [PRSP-SUBSET-Filters-Template-DrawerActions-Fieldset] -->
198
204
  <div class="prsp-filters-actions">
205
+ <label class="prsp-filters-showdeleted" id="PRSP_ShowDeleted_Host" title="Include soft-deleted records in the results"></label>
199
206
  <button type="button" class="prsp-filters-btn-text" id="PRSP_Filter_Button_Clear" title="Clear all filters to a blank state" onclick="_Pict.views['PRSP-Filters'].handleClear(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}')">Clear</button>
200
207
  <button type="button" class="prsp-filters-btn-text" id="PRSP_Filter_Button_Reset" title="Reset all filters to the last saved/defaulted state" onclick="_Pict.views['PRSP-Filters'].handleReset(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}')">Reset</button>
201
208
  <button type="button" class="prsp-filters-apply" id="PRSP_Filter_Button_ApplyDrawer" onclick="_Pict.views['PRSP-Filters'].handleSearch(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}')">Apply</button>
@@ -286,6 +293,7 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
286
293
  {~TS:PRSP-QuickFilter-Text:Record.TextSlot~}
287
294
  {~TS:PRSP-QuickFilter-Date:Record.DateSlot~}
288
295
  {~TS:PRSP-QuickFilter-Entity:Record.EntitySlot~}
296
+ {~TS:PRSP-QuickFilter-Distinct:Record.DistinctSlot~}
289
297
  </div>
290
298
  `
291
299
  },
@@ -318,6 +326,14 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
318
326
  Hash: 'PRSP-QuickFilter-Entity',
319
327
  Template: /*html*/`
320
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>
321
337
  `
322
338
  },
323
339
  ],
@@ -630,10 +646,11 @@ class ViewRecordSetSUBSETFilters extends libPictView
630
646
  // `quickFiltersAutoDefault` (host-settable, default on) gates the clever schema defaults: a host
631
647
  // can set it false to make quick filters opt-in (only record sets with an explicit config show).
632
648
  const tmpEntityMounts = [];
649
+ const tmpDistinctMounts = [];
633
650
  const tmpItems = tmpProvider.getQuickFilterDefinitions(this.quickFiltersAutoDefault).map((pDefinition) =>
634
651
  {
635
652
  const tmpBase = { Field: pDefinition.Field, ClauseKey: pDefinition.ClauseKey, Label: pDefinition.Label, RecordSet: pRecordSet, ViewContext: pViewContext };
636
- const tmpItem = { Label: pDefinition.Label, TextSlot: [], DateSlot: [], EntitySlot: [] };
653
+ const tmpItem = { Label: pDefinition.Label, TextSlot: [], DateSlot: [], EntitySlot: [], DistinctSlot: [] };
637
654
  if (pDefinition.Control === 'text')
638
655
  {
639
656
  tmpItem.TextSlot = [ Object.assign({}, tmpBase, { Value: tmpProvider.getQuickFilterClauseValue(pDefinition.Field), Placeholder: `Search ${pDefinition.Label}…` }) ];
@@ -649,12 +666,19 @@ class ViewRecordSetSUBSETFilters extends libPictView
649
666
  tmpItem.EntitySlot = [ Object.assign({}, tmpBase, { HostID: tmpHostID }) ];
650
667
  tmpEntityMounts.push(Object.assign({}, tmpBase, { HostID: tmpHostID }));
651
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
+ }
652
675
  return tmpItem;
653
676
  });
654
677
  const tmpHTML = (tmpItems.length > 0) ? this.pict.parseTemplateByHash('PRSP-QuickFilters-Bar', { Filters: tmpItems }) : '';
655
678
  this.pict.ContentAssignment.assignContent('#PRSP_QuickFilters', tmpHTML);
656
- // 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.
657
680
  tmpEntityMounts.forEach((pMount) => this._mountQuickFilterEntity(pRecordSet, pViewContext, pMount));
681
+ tmpDistinctMounts.forEach((pMount) => this._mountQuickFilterDistinct(pRecordSet, pViewContext, pMount));
658
682
  }
659
683
 
660
684
  /**
@@ -713,12 +737,107 @@ class ViewRecordSetSUBSETFilters extends libPictView
713
737
  tmpView.setValue((Array.isArray(tmpCurrent) && tmpCurrent.length > 0) ? tmpCurrent[0] : '');
714
738
  }
715
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
+
716
790
  /**
717
791
  * Apply a text quick filter: upsert (or clear) its tagged clause, then run the standard search +
718
792
  * serialize path. Commits on blur / Enter (not per-keystroke) so the re-render never steals focus.
719
793
  *
720
794
  * @param {string} pRecordSet @param {string} pViewContext @param {string} pField @param {string} pClauseKey @param {string} pValue
721
795
  */
796
+ /**
797
+ * Flip the show-deleted switch (the drawer-footer checkbox, RecordSetListShowDeletedFilter
798
+ * recordsets). The switch is a REAL clause — a RawFilter referencing the Deleted column, which
799
+ * suppresses the automatic `Deleted = 0` so soft-deleted rows enumerate — upserted into the
800
+ * active filter state and applied through the normal search flow. Because the clause changes
801
+ * the serialized filter experience, the route URL always changes: the fetch reliably fires,
802
+ * and the state survives reloads and shared links. Clear/Reset drop it like any clause.
803
+ *
804
+ * @param {string} pRecordSet - The record set the toggle belongs to
805
+ * @param {string} pViewContext - The view context (List, Dashboard)
806
+ * @param {boolean} pChecked - Whether deleted records should be included
807
+ */
808
+ toggleShowDeletedFilter(pRecordSet, pViewContext, pChecked)
809
+ {
810
+ const tmpProvider = this.pict.providers['RSP-Provider-' + pRecordSet];
811
+ if (!tmpProvider || typeof tmpProvider.setShowDeletedFilterValue !== 'function')
812
+ {
813
+ return;
814
+ }
815
+ tmpProvider.setShowDeletedFilterValue(pChecked === true);
816
+ this.handleSearch(null, pRecordSet, pViewContext);
817
+ }
818
+
819
+ /**
820
+ * Paint the show-deleted checkbox into its drawer-footer host (next to Clear/Reset/Apply),
821
+ * seeded from the clause's presence. Painted post-render like the quick bar; empty when the
822
+ * record set hasn't opted in via RecordSetListShowDeletedFilter.
823
+ *
824
+ * @param {string} pRecordSet @param {string} pViewContext
825
+ */
826
+ _renderShowDeletedControl(pRecordSet, pViewContext)
827
+ {
828
+ if (!document.getElementById('PRSP_ShowDeleted_Host')) { return; }
829
+ const tmpRecordSetConfiguration = this.pict.PictSectionRecordSet?.recordSetProviderConfigurations?.[pRecordSet] || {};
830
+ if (tmpRecordSetConfiguration.RecordSetListShowDeletedFilter !== true)
831
+ {
832
+ this.pict.ContentAssignment.assignContent('#PRSP_ShowDeleted_Host', '');
833
+ return;
834
+ }
835
+ const tmpProvider = this.pict.providers['RSP-Provider-' + pRecordSet];
836
+ const tmpChecked = (tmpProvider && typeof tmpProvider.getShowDeletedFilterValue === 'function' && tmpProvider.getShowDeletedFilterValue()) ? 'checked' : '';
837
+ this.pict.ContentAssignment.assignContent('#PRSP_ShowDeleted_Host',
838
+ `<input class="prsp-filters-showdeleted-checkbox" type="checkbox" ${tmpChecked} onchange="_Pict.views['PRSP-Filters'].toggleShowDeletedFilter('${pRecordSet}', '${pViewContext}', this.checked)"> Show deleted`);
839
+ }
840
+
722
841
  applyQuickFilterText(pRecordSet, pViewContext, pField, pClauseKey, pValue)
723
842
  {
724
843
  this.bumpRenderEpoch();
@@ -768,6 +887,23 @@ class ViewRecordSetSUBSETFilters extends libPictView
768
887
  }
769
888
  }
770
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
+
771
907
  /**
772
908
  * @param {Event} pEvent - The DOM event that triggered the search
773
909
  * @param {string} pRecordSet - The record set being filtered
@@ -1162,7 +1298,8 @@ class ViewRecordSetSUBSETFilters extends libPictView
1162
1298
  if (tmpFilterRecordSet)
1163
1299
  {
1164
1300
  this._paintFilterControls(tmpFilterRecordSet);
1165
- this._renderQuickFilters(tmpFilterRecordSet, tmpFilterViewContext);
1301
+ this._renderQuickFilters(tmpFilterRecordSet, tmpFilterViewContext);
1302
+ this._renderShowDeletedControl(tmpFilterRecordSet, tmpFilterViewContext);
1166
1303
  // (Re)render the experiences dropdown only when its container is empty — i.e. on
1167
1304
  // a fresh filter render — not on every sub-render (add-filter dropdown, etc.).
1168
1305
  const tmpExpContainer = document.getElementById('FilterPersistenceView-Container');
@@ -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'),