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.
- package/README.md +38 -0
- package/package.json +2 -2
- package/source/Pict-Section-RecordSet.js +1 -0
- package/source/providers/Column-Data-Provider.js +219 -0
- package/source/providers/RecordSet-RecordProvider-Base.js +64 -1
- package/source/providers/RecordSet-RecordProvider-MeadowEndpoints.js +92 -16
- package/source/services/RecordsSet-MetaController.js +23 -1
- package/source/templates/Pict-Template-FilterInstanceViews.js +4 -0
- package/source/views/RecordSet-Filters.js +140 -3
- package/source/views/filters/RecordSet-Filter-DistinctSelectedValueList.js +233 -0
- package/source/views/filters/index.js +2 -0
- package/source/views/list/RecordSet-List-ColumnChooser.js +345 -0
- package/source/views/list/RecordSet-List-RecordListEntry.js +4 -1
- package/source/views/list/RecordSet-List.js +390 -15
- package/source/views/read/RecordSet-Read.js +65 -6
- package/types/Pict-Section-RecordSet.d.ts +1 -0
- package/types/providers/Column-Data-Provider.d.ts +115 -0
- package/types/providers/Column-Data-Provider.d.ts.map +1 -0
- package/types/providers/RecordSet-DynamicRecordsetSolver.d.ts +3 -0
- package/types/providers/RecordSet-DynamicRecordsetSolver.d.ts.map +1 -1
- package/types/providers/RecordSet-RecordProvider-Base.d.ts +110 -0
- package/types/providers/RecordSet-RecordProvider-Base.d.ts.map +1 -1
- package/types/providers/RecordSet-RecordProvider-MeadowEndpoints.d.ts +51 -1
- package/types/providers/RecordSet-RecordProvider-MeadowEndpoints.d.ts.map +1 -1
- package/types/providers/RecordSet-Router.d.ts +1 -0
- package/types/providers/RecordSet-Router.d.ts.map +1 -1
- package/types/services/RecordsSet-MetaController.d.ts.map +1 -1
- package/types/templates/Pict-Template-FilterInstanceViews.d.ts.map +1 -1
- package/types/views/RecordSet-Filters.d.ts +61 -0
- package/types/views/RecordSet-Filters.d.ts.map +1 -1
- package/types/views/RecordSet-RecordBaseView.d.ts +1 -0
- package/types/views/RecordSet-RecordBaseView.d.ts.map +1 -1
- package/types/views/filters/RecordSet-Filter-EntityReference-Base.d.ts.map +1 -1
- package/types/views/list/RecordSet-List-ColumnChooser.d.ts +68 -0
- package/types/views/list/RecordSet-List-ColumnChooser.d.ts.map +1 -0
- package/types/views/list/RecordSet-List-RecordListEntry.d.ts.map +1 -1
- package/types/views/list/RecordSet-List.d.ts +167 -2
- package/types/views/list/RecordSet-List.d.ts.map +1 -1
- package/types/views/read/RecordSet-Read.d.ts +8 -0
- 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
|
-
|
|
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'),
|