pict-section-recordset 1.0.69 → 1.1.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 +6 -4
- package/docs/README.md +6 -6
- package/docs/_cover.md +1 -1
- package/docs/_version.json +3 -3
- package/docs/index.html +6 -5
- package/docs/retold-catalog.json +84 -216
- package/docs/retold-keyword-index.json +3177 -1215
- package/example_applications/ServeExamples.js +1 -1
- package/package.json +6 -6
- package/source/providers/Filter-Data-Provider.js +16 -5
- package/source/views/Filter-PersistenceView.js +150 -35
- package/source/views/RecordSet-Filters.js +212 -28
- package/source/views/list/RecordSet-List.js +16 -0
|
@@ -59,7 +59,7 @@ tmpAnticipate.anticipate(
|
|
|
59
59
|
const libOratorHTTPProxy = require(`orator-http-proxy`);
|
|
60
60
|
_Fable.serviceManager.addServiceType('OratorHTTPProxy', libOratorHTTPProxy);
|
|
61
61
|
_Fable.serviceManager.instantiateServiceProvider('OratorHTTPProxy', {LogLevel: 2});
|
|
62
|
-
// Proxy all /1.0/ requests to the locally-running bookstore service (you need to run this from https://github.com/
|
|
62
|
+
// Proxy all /1.0/ requests to the locally-running bookstore service (you need to run this from https://github.com/fable-retold/retold-harness ... it's a one-liner to start the service)
|
|
63
63
|
tmpAnticipate.anticipate(
|
|
64
64
|
(fNext)=>
|
|
65
65
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-section-recordset",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Pict dynamic record set management views",
|
|
5
5
|
"main": "source/Pict-Section-RecordSet.js",
|
|
6
6
|
"directories": {
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/
|
|
11
|
+
"url": "git+https://github.com/fable-retold/pict-section-recordset.git"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/
|
|
14
|
+
"url": "https://github.com/fable-retold/pict-section-recordset/issues"
|
|
15
15
|
},
|
|
16
|
-
"homepage": "https://github.com/
|
|
16
|
+
"homepage": "https://github.com/fable-retold/pict-section-recordset#readme",
|
|
17
17
|
"scripts": {
|
|
18
18
|
"start": "node source/Pict-Section-RecordSet.js",
|
|
19
19
|
"tests": "npx quack test -g",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"jquery": "^3.7.1",
|
|
36
36
|
"pict": "^1.0.372",
|
|
37
37
|
"pict-application": "^1.0.34",
|
|
38
|
-
"pict-docuserve": "^1.
|
|
38
|
+
"pict-docuserve": "^1.4.4",
|
|
39
39
|
"pict-service-commandlineutility": "^1.0.19",
|
|
40
|
-
"quackage": "^1.
|
|
40
|
+
"quackage": "^1.3.0",
|
|
41
41
|
"typescript": "^5.9.3"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
@@ -128,10 +128,17 @@ class FilterDataProvider extends libPictProvider
|
|
|
128
128
|
*/
|
|
129
129
|
navigateToFilterExperienceRoute(tmpFilterExperience, pRecordSet, pViewContext)
|
|
130
130
|
{
|
|
131
|
-
//
|
|
131
|
+
// The search term (the /FilteredTo/ segment) is part of the saved experience — restore
|
|
132
|
+
// it alongside the structured filter clauses.
|
|
133
|
+
const tmpSearchSegment = (tmpFilterExperience && tmpFilterExperience.SearchFilterURLParam) ? `/FilteredTo/${tmpFilterExperience.SearchFilterURLParam}` : '';
|
|
134
|
+
// go to the new url with the search segment + the filter experience encoded param
|
|
132
135
|
if (tmpFilterExperience?.FilterExperienceEncodedURLParam && tmpFilterExperience?.FilterExperienceEncodedURLParam?.length > 0)
|
|
133
136
|
{
|
|
134
|
-
this.fable.providers.RecordSetRouter.pictRouter.navigate(`/PSRS/${pRecordSet}/${pViewContext}/FilterExperience/${tmpFilterExperience.FilterExperienceEncodedURLParam}`);
|
|
137
|
+
this.fable.providers.RecordSetRouter.pictRouter.navigate(`/PSRS/${pRecordSet}/${pViewContext}${tmpSearchSegment}/FilterExperience/${tmpFilterExperience.FilterExperienceEncodedURLParam}`);
|
|
138
|
+
}
|
|
139
|
+
else if (tmpSearchSegment)
|
|
140
|
+
{
|
|
141
|
+
this.fable.providers.RecordSetRouter.pictRouter.navigate(`/PSRS/${pRecordSet}/${pViewContext}${tmpSearchSegment}`);
|
|
135
142
|
}
|
|
136
143
|
else
|
|
137
144
|
{
|
|
@@ -431,10 +438,12 @@ class FilterDataProvider extends libPictProvider
|
|
|
431
438
|
* @param {string} pViewContext - The current view context
|
|
432
439
|
* @return {boolean} - Returns true when the settings have been saved.
|
|
433
440
|
*/
|
|
434
|
-
saveFilterMeta(pRecordSet, pViewContext)
|
|
441
|
+
saveFilterMeta(pRecordSet, pViewContext, pSilent, pDisplayNameOverride)
|
|
435
442
|
{
|
|
436
443
|
const activeFilterExperienceClauses = this.pict.Bundle._ActiveFilterState[pRecordSet]?.FilterClauses || [];
|
|
437
|
-
const filterDisplayName =
|
|
444
|
+
const filterDisplayName = (pDisplayNameOverride && String(pDisplayNameOverride).trim())
|
|
445
|
+
? String(pDisplayNameOverride).trim()
|
|
446
|
+
: this.getCurrentFilterName({ FilterClauses: activeFilterExperienceClauses }, pRecordSet, pViewContext);
|
|
438
447
|
const tmpFilterExperienceHash = filterDisplayName.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
439
448
|
|
|
440
449
|
if (this.checkIfFilterExperienceExists(pRecordSet, pViewContext, tmpFilterExperienceHash))
|
|
@@ -444,13 +453,15 @@ class FilterDataProvider extends libPictProvider
|
|
|
444
453
|
}
|
|
445
454
|
|
|
446
455
|
// TODO: BUG: Gotta have a more complex merge happen here for multiple tabs
|
|
447
|
-
const newFilterExperience = {
|
|
456
|
+
const newFilterExperience = {
|
|
448
457
|
RecordSet: pRecordSet,
|
|
449
458
|
ViewContext: pViewContext,
|
|
450
459
|
FilterClauses: activeFilterExperienceClauses,
|
|
451
460
|
FilterDisplayName: filterDisplayName,
|
|
452
461
|
FilterExperienceHash: tmpFilterExperienceHash,
|
|
453
462
|
FilterExperienceEncodedURLParam: window.location.hash.split('/FilterExperience/')?.[1] || '',
|
|
463
|
+
// The search term (the /FilteredTo/ segment) is part of the saved experience too.
|
|
464
|
+
SearchFilterURLParam: window.location.hash.split('/FilteredTo/')?.[1]?.split('/FilterExperience/')?.[0] || '',
|
|
454
465
|
LastModifiedDate: new Date().toISOString(),
|
|
455
466
|
}
|
|
456
467
|
// Save the specific filter metadata to localStorage
|
|
@@ -21,7 +21,20 @@ const _DEFAULT_CONFIGURATION_FilterPersistenceView = (
|
|
|
21
21
|
AutoSolveWithApp: false,
|
|
22
22
|
AutoSolveOrdinal: 0,
|
|
23
23
|
|
|
24
|
-
CSS:
|
|
24
|
+
CSS: /*css*/`
|
|
25
|
+
.prsp-exp { display: flex; flex-direction: column; gap: 0.4rem; }
|
|
26
|
+
.prsp-exp-label { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--theme-color-text-muted, #6b7686); }
|
|
27
|
+
.prsp-exp-row { display: flex; align-items: center; gap: 0.5rem; }
|
|
28
|
+
.prsp-exp-row > span:empty { display: none; }
|
|
29
|
+
.prsp-exp-select, .prsp-exp-name { font: inherit; font-size: 0.9rem; padding: 0.42rem 0.6rem; border-radius: 8px;
|
|
30
|
+
border: 1px solid var(--theme-color-border-default, #d7dce3); background: var(--theme-color-background-primary, #fff); color: var(--theme-color-text-primary, #1f2733); }
|
|
31
|
+
.prsp-exp-select { flex: 0 0 auto; width: 230px; min-width: 0; max-width: 100%; }
|
|
32
|
+
.prsp-exp-select:focus, .prsp-exp-name:focus { outline: none; border-color: var(--theme-color-brand-primary, #156dd1);
|
|
33
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--theme-color-brand-primary, #156dd1) 16%, transparent); }
|
|
34
|
+
.prsp-exp-btn { flex: 0 0 auto; white-space: nowrap; }
|
|
35
|
+
.prsp-exp-validation { font-size: 0.8rem; color: var(--theme-color-status-error, #cf5b5b); }
|
|
36
|
+
.prsp-exp-validation:empty { display: none; }
|
|
37
|
+
`,
|
|
25
38
|
CSSPriority: 500,
|
|
26
39
|
|
|
27
40
|
Templates:
|
|
@@ -30,41 +43,17 @@ const _DEFAULT_CONFIGURATION_FilterPersistenceView = (
|
|
|
30
43
|
Hash: 'FilterPersistenceView-Container',
|
|
31
44
|
Template: /*html*/`
|
|
32
45
|
<!-- DefaultPackage pict view template: [FilterPersistenceView-Container] -->
|
|
33
|
-
<div id="FilterPersistenceView-Content">
|
|
34
|
-
|
|
35
|
-
<div
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<input type="text" id="FilterPersistenceView-CurrentFilterNameInput" name="CurrentFilterName" value="" onfocus="this.select()" />
|
|
43
|
-
<button type="button" id="FilterPersistenceView-SaveFilterButton" onclick="_Pict.views['FilterPersistenceView'].saveFilterPersistenceSettings(event)">
|
|
44
|
-
<span id="FilterPersistenceView-SaveFilterButtonText">Save</span>
|
|
45
|
-
</button>
|
|
46
|
-
</div>
|
|
47
|
-
<div class="FilterPersistenceView-StoredSettings">
|
|
48
|
-
<label for="StoredFilterName">Stored Filter Experiences:</label>
|
|
49
|
-
<select id="FilterPersistenceView-StoredFiltersSelect" onchange="_Pict.views['FilterPersistenceView'].setFilterExperienceToSelection(event)" name="StoredFilterName">
|
|
50
|
-
<!-- Options will be populated dynamically -->
|
|
51
|
-
</select>
|
|
52
|
-
<button type="button" id="FilterPersistenceView-LoadFilterButton" onclick="_Pict.views['FilterPersistenceView'].loadFilterPersistenceSettings(event)">Load</button>
|
|
53
|
-
<button type="button" id="FilterPersistenceView-SetAsDefaultButton" onclick="_Pict.views['FilterPersistenceView'].toggleFilterExperienceAsTheDefault(event, true)">Set As Default</button>
|
|
54
|
-
<button type="button" id="FilterPersistenceView-RemoveAsDefaultButton" onclick="_Pict.views['FilterPersistenceView'].toggleFilterExperienceAsTheDefault(event, false)">Remove As Default</button>
|
|
55
|
-
<button type="button" id="FilterPersistenceView-DeleteFilterButton" onclick="_Pict.views['FilterPersistenceView'].deleteFilterPersistenceSettings(event)">Delete</button>
|
|
56
|
-
</div>
|
|
57
|
-
<div class="FilterPersistenceView-OptionalSettings">
|
|
58
|
-
<label for="OptionalSettings">Optional Settings:</label>
|
|
59
|
-
<label for="OptionalSettings-RememberLastUsed">
|
|
60
|
-
<input type="checkbox" id="OptionalSettings-RememberLastUsed" name="RememberLastUsed" onchange="_Pict.views['FilterPersistenceView'].toggleRememberLastUsedFilterExperience(event)" />
|
|
61
|
-
Remember my last search filter experience
|
|
62
|
-
</label>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
<div id="FilterPersistenceView-Footer">
|
|
66
|
-
<button type="button" id="FilterPersistenceView-CloseManageFiltersButton" onclick="_Pict.views['FilterPersistenceView'].closeFilterPersistenceUI()">Close</button>
|
|
46
|
+
<div id="FilterPersistenceView-Content" class="prsp-exp">
|
|
47
|
+
<span class="prsp-exp-label">Filter experience</span>
|
|
48
|
+
<div class="prsp-exp-row">
|
|
49
|
+
<select id="FilterPersistenceView-StoredFiltersSelect" class="prsp-exp-select" name="StoredFilterName" title="Load a saved filter experience"
|
|
50
|
+
onchange="_Pict.views['FilterPersistenceView'].setFilterExperienceToSelection(event); _Pict.views['FilterPersistenceView'].loadFilterPersistenceSettings(event);">
|
|
51
|
+
<!-- Options populated dynamically -->
|
|
52
|
+
</select>
|
|
53
|
+
<button type="button" class="prsp-filters-btn-text prsp-exp-btn" id="FilterPersistenceView-SaveFilterButton" title="Save the current search + filters as a named experience" onclick="_Pict.views['FilterPersistenceView'].promptSaveFilterExperience(event)">Save</button>
|
|
54
|
+
<button type="button" class="prsp-filters-btn-text prsp-exp-btn" id="FilterPersistenceView-DeleteFilterButton" title="Delete the selected experience" onclick="_Pict.views['FilterPersistenceView'].deleteFilterPersistenceSettings(event)">Delete</button>
|
|
67
55
|
</div>
|
|
56
|
+
<span id="FilterPersistenceView-CurrentFilterNameInput-ValidationMessage" class="prsp-exp-validation"></span>
|
|
68
57
|
</div>
|
|
69
58
|
<!-- DefaultPackage end view template: [FilterPersistenceView-Container] -->
|
|
70
59
|
`
|
|
@@ -97,6 +86,24 @@ class viewFilterPersistenceView extends libPictView
|
|
|
97
86
|
this.filterExperienceSelection = null;
|
|
98
87
|
this.filterExperienceInitialized = false;
|
|
99
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Re-assert this view's own template before rendering so a host/server template override
|
|
92
|
+
* doesn't replace the slimmed experiences control (mirrors RecordSet-Filters).
|
|
93
|
+
*
|
|
94
|
+
* @param {import('pict-view').Renderable} pRenderable
|
|
95
|
+
*/
|
|
96
|
+
onBeforeRender(pRenderable)
|
|
97
|
+
{
|
|
98
|
+
if (this.pict.TemplateProvider && Array.isArray(_DEFAULT_CONFIGURATION_FilterPersistenceView.Templates))
|
|
99
|
+
{
|
|
100
|
+
for (const tmpTemplate of _DEFAULT_CONFIGURATION_FilterPersistenceView.Templates)
|
|
101
|
+
{
|
|
102
|
+
this.pict.TemplateProvider.addTemplate(tmpTemplate.Hash, tmpTemplate.Template);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return super.onBeforeRender(pRenderable);
|
|
106
|
+
}
|
|
100
107
|
|
|
101
108
|
|
|
102
109
|
/**
|
|
@@ -408,6 +415,114 @@ class viewFilterPersistenceView extends libPictView
|
|
|
408
415
|
* @param {function} [pCallback] - A callback function to be executed after saving the settings.
|
|
409
416
|
* @returns {boolean} - Returns true when the settings have been saved.
|
|
410
417
|
*/
|
|
418
|
+
/** HTML-escape a value for safe display in the modal. */
|
|
419
|
+
_escapeHTML(pValue)
|
|
420
|
+
{
|
|
421
|
+
return String(pValue == null ? '' : pValue).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Describe a single active filter clause in plain English (e.g. "Date Created between A and B"). */
|
|
425
|
+
_describeFilterClause(pClause)
|
|
426
|
+
{
|
|
427
|
+
const tmpLabel = pClause.Label || pClause.FilterByColumn || 'Filter';
|
|
428
|
+
const tmpType = String(pClause.Type || '');
|
|
429
|
+
const tmpValues = Array.isArray(pClause.Values) ? pClause.Values.filter((v) => v !== null && v !== undefined && v !== '') : [];
|
|
430
|
+
const tmpValue = (pClause.Value !== null && pClause.Value !== undefined && pClause.Value !== '') ? pClause.Value : null;
|
|
431
|
+
if (/Range/i.test(tmpType))
|
|
432
|
+
{
|
|
433
|
+
const tmpLow = (tmpValues.length > 0) ? tmpValues[0] : (tmpValue != null ? tmpValue : '…');
|
|
434
|
+
const tmpHigh = (tmpValues.length > 1) ? tmpValues[1] : '…';
|
|
435
|
+
return `${tmpLabel} between ${tmpLow} and ${tmpHigh}`;
|
|
436
|
+
}
|
|
437
|
+
if (/Select/i.test(tmpType))
|
|
438
|
+
{
|
|
439
|
+
const tmpList = tmpValues.length ? tmpValues : (tmpValue != null ? [ tmpValue ] : []);
|
|
440
|
+
return tmpList.length ? `${tmpLabel} is ${tmpList.join(', ')}` : `${tmpLabel} is set`;
|
|
441
|
+
}
|
|
442
|
+
const tmpSingle = (tmpValue != null) ? tmpValue : (tmpValues.length ? tmpValues.join(', ') : null);
|
|
443
|
+
const tmpOp = pClause.ExactMatch ? 'is' : 'contains';
|
|
444
|
+
return (tmpSingle != null) ? `${tmpLabel} ${tmpOp} "${tmpSingle}"` : `${tmpLabel} is set`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* @returns {{ summary: string, count: string }} A plain-English summary of the current
|
|
449
|
+
* search + filter clauses, and the matched record count (from the rendered list total).
|
|
450
|
+
*/
|
|
451
|
+
_describeCurrentFilter()
|
|
452
|
+
{
|
|
453
|
+
const tmpParts = [];
|
|
454
|
+
const tmpFilterView = this.pict.views['PRSP-Filters'];
|
|
455
|
+
const tmpSearchTerm = (tmpFilterView && typeof tmpFilterView._searchTermFromURL === 'function') ? tmpFilterView._searchTermFromURL() : '';
|
|
456
|
+
if (tmpSearchTerm)
|
|
457
|
+
{
|
|
458
|
+
const tmpCfg = this.pict.PictSectionRecordSet && this.pict.PictSectionRecordSet.recordSetProviderConfigurations
|
|
459
|
+
? this.pict.PictSectionRecordSet.recordSetProviderConfigurations[this.currentRecordSet] : null;
|
|
460
|
+
const tmpFields = (tmpCfg && Array.isArray(tmpCfg.SearchFields) && tmpCfg.SearchFields.length) ? tmpCfg.SearchFields : [ 'Name' ];
|
|
461
|
+
tmpParts.push(`${tmpFields.join(' / ')} like "${tmpSearchTerm}"`);
|
|
462
|
+
}
|
|
463
|
+
const tmpClauses = (this.pict.Bundle._ActiveFilterState[this.currentRecordSet] || {}).FilterClauses || [];
|
|
464
|
+
for (const tmpClause of tmpClauses) { tmpParts.push(this._describeFilterClause(tmpClause)); }
|
|
465
|
+
const tmpSummary = tmpParts.length ? (tmpParts.join('. ') + '.') : 'No filters — matches all records.';
|
|
466
|
+
const tmpCountEl = document.getElementById('PRSP_Pagination_Description_Records_Total');
|
|
467
|
+
const tmpCount = (tmpCountEl && tmpCountEl.textContent) ? tmpCountEl.textContent.trim() : '';
|
|
468
|
+
return { summary: tmpSummary, count: tmpCount };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Prompt for a name (via pict-section-modal when available) and save the current search +
|
|
473
|
+
* filters as a named experience. The modal previews the filter in plain English and the
|
|
474
|
+
* number of records it matches. Falls back to the generated name with no prompt if the
|
|
475
|
+
* modal section is not registered in the host app.
|
|
476
|
+
*
|
|
477
|
+
* @param {Event} pEvent
|
|
478
|
+
*/
|
|
479
|
+
promptSaveFilterExperience(pEvent)
|
|
480
|
+
{
|
|
481
|
+
if (pEvent) { pEvent.preventDefault(); pEvent.stopPropagation(); }
|
|
482
|
+
if (!this.currentRecordSet || !this.currentViewContext) { return false; }
|
|
483
|
+
const tmpProvider = this.pict.providers.FilterDataProvider;
|
|
484
|
+
if (tmpProvider.filterExperienceModifiedFromURLHash)
|
|
485
|
+
{
|
|
486
|
+
this.pict.log.warn('The current filter experience was modified from the URL hash; apply or reset before saving.');
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
const tmpClauses = (this.pict.Bundle._ActiveFilterState[this.currentRecordSet] || {}).FilterClauses || [];
|
|
490
|
+
const tmpCurrent = this.filterExperienceSelection ? tmpProvider.getFilterExperienceByHash(this.currentRecordSet, this.currentViewContext, this.filterExperienceSelection) : null;
|
|
491
|
+
const tmpSuggested = (tmpCurrent && tmpCurrent.FilterDisplayName) || tmpProvider.generateContextualDefaultFilterName({ FilterClauses: tmpClauses }, this.currentRecordSet, this.currentViewContext);
|
|
492
|
+
|
|
493
|
+
const fSave = (pName) =>
|
|
494
|
+
{
|
|
495
|
+
tmpProvider.saveFilterMeta(this.currentRecordSet, this.currentViewContext, false, pName);
|
|
496
|
+
this.buildSelectOptionsForAvailableFilterExperiences();
|
|
497
|
+
this.pict.log.info(`Filter experience '${pName}' saved.`);
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const tmpModal = this.pict.views['Pict-Section-Modal'];
|
|
501
|
+
if (!tmpModal || typeof tmpModal.show !== 'function')
|
|
502
|
+
{
|
|
503
|
+
fSave(tmpSuggested);
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
const tmpEscaped = this._escapeHTML(tmpSuggested);
|
|
507
|
+
const tmpDescription = this._describeCurrentFilter();
|
|
508
|
+
const tmpCountLine = tmpDescription.count
|
|
509
|
+
? `<p style="margin:0 0 0.8rem;color:var(--theme-color-text-secondary,#45505f);">Matches <strong>${this._escapeHTML(tmpDescription.count)}</strong> record${tmpDescription.count === '1' ? '' : 's'}.</p>`
|
|
510
|
+
: '';
|
|
511
|
+
tmpModal.show(
|
|
512
|
+
{
|
|
513
|
+
title: 'Save filter experience',
|
|
514
|
+
content: `<p style="margin:0 0 0.75rem;color:var(--theme-color-text-secondary,#45505f);font-size:0.9rem;line-height:1.45;">Give this filter a name to save it, then load it again any time to come back to these records, or whatever matches it down the road.</p><div style="margin-bottom:0.6rem;padding:0.6rem 0.8rem;border-radius:8px;background:var(--theme-color-background-tertiary,#eceef2);font-size:0.9rem;line-height:1.4;color:var(--theme-color-text-primary,#1f2733);">${this._escapeHTML(tmpDescription.summary)}</div>${tmpCountLine}<p style="margin-bottom:0.4rem;">Name this filter experience:</p><input type="text" id="prsp-exp-name-input" class="pict-input" style="width:100%;" value="${tmpEscaped}" autofocus>`,
|
|
515
|
+
buttons: [ { Hash: 'cancel', Label: 'Cancel' }, { Hash: 'ok', Label: 'Save', Style: 'primary' } ],
|
|
516
|
+
}).then((pChoice) =>
|
|
517
|
+
{
|
|
518
|
+
if (pChoice !== 'ok') { return; }
|
|
519
|
+
const tmpEl = document.getElementById('prsp-exp-name-input');
|
|
520
|
+
const tmpName = (tmpEl && tmpEl.value && tmpEl.value.trim()) ? tmpEl.value.trim() : tmpSuggested;
|
|
521
|
+
fSave(tmpName);
|
|
522
|
+
});
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
|
|
411
526
|
saveFilterPersistenceSettings(event, pCallback)
|
|
412
527
|
{
|
|
413
528
|
event.preventDefault();
|
|
@@ -22,24 +22,80 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
|
|
|
22
22
|
AutoSolveWithApp: false,
|
|
23
23
|
AutoSolveOrdinal: 0,
|
|
24
24
|
|
|
25
|
-
CSS:
|
|
25
|
+
CSS: /*css*/`
|
|
26
|
+
.prsp-filters { width: 100%; }
|
|
27
|
+
.prsp-filters *, .prsp-filters *::before, .prsp-filters *::after { box-sizing: border-box; }
|
|
28
|
+
.prsp-filters-bar { display: flex; align-items: center; gap: 0.5rem; margin: 0; }
|
|
29
|
+
.prsp-filters-search { position: relative; flex: 1 1 auto; display: flex; align-items: center; min-width: 0; }
|
|
30
|
+
.prsp-filters-search-ic { position: absolute; left: 0.75rem; display: inline-flex; align-items: center; color: var(--theme-color-text-muted, #6b7686); pointer-events: none; font-size: 0.95rem; }
|
|
31
|
+
.prsp-filters-input { width: 100%; font: inherit; font-size: 0.95rem; padding: 0.5rem 0.85rem 0.5rem 2.2rem;
|
|
32
|
+
border: 1px solid var(--theme-color-border-default, #d7dce3); border-radius: 8px;
|
|
33
|
+
background: var(--theme-color-background-primary, #fff); color: var(--theme-color-text-primary, #1f2733); }
|
|
34
|
+
.prsp-filters-input:focus { outline: none; border-color: var(--theme-color-brand-primary, #156dd1);
|
|
35
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--theme-color-brand-primary, #156dd1) 16%, transparent); }
|
|
36
|
+
.prsp-filters-toggle { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 0.4rem; font: inherit; cursor: pointer;
|
|
37
|
+
padding: 0.45rem 0.7rem; border-radius: 8px; border: 1px solid var(--theme-color-border-default, #d7dce3);
|
|
38
|
+
background: var(--theme-color-background-primary, #fff); color: var(--theme-color-text-secondary, #45505f); }
|
|
39
|
+
.prsp-filters-toggle:hover { border-color: var(--theme-color-brand-primary, #156dd1); }
|
|
40
|
+
.prsp-filters.has-filters .prsp-filters-toggle { border-color: var(--theme-color-brand-primary, #156dd1); color: var(--theme-color-brand-primary, #156dd1); }
|
|
41
|
+
.prsp-filters.drawer-open .prsp-filters-toggle { background: var(--theme-color-background-tertiary, #eceef2); }
|
|
42
|
+
.prsp-filters-toggle-ic { display: inline-flex; align-items: center; }
|
|
43
|
+
.prsp-filters-toggle-ic svg { width: 1.05em; height: 1.05em; display: block; }
|
|
44
|
+
.prsp-filters-toggle-count { display: none; align-items: center; justify-content: center; min-width: 1.3em; height: 1.3em;
|
|
45
|
+
padding: 0 0.35em; border-radius: 999px; font-size: 0.74rem; font-weight: 700;
|
|
46
|
+
background: var(--theme-color-brand-primary, #156dd1); color: var(--theme-color-text-on-brand, #fff); }
|
|
47
|
+
.prsp-filters.has-filters .prsp-filters-toggle-count { display: inline-flex; }
|
|
48
|
+
.prsp-filters-apply { flex: 0 0 auto; font: inherit; font-weight: 650; cursor: pointer; padding: 0.5rem 1.15rem; border-radius: 8px;
|
|
49
|
+
border: 1px solid var(--theme-color-brand-primary, #156dd1); background: var(--theme-color-brand-primary, #156dd1); color: var(--theme-color-text-on-brand, #fff); }
|
|
50
|
+
.prsp-filters-apply:hover { background: color-mix(in srgb, var(--theme-color-brand-primary, #156dd1) 88%, #000); }
|
|
51
|
+
.prsp-filters-btn-text { font: inherit; cursor: pointer; padding: 0.4rem 0.85rem; border-radius: 8px;
|
|
52
|
+
border: 1px solid var(--theme-color-border-default, #d7dce3); background: transparent; color: var(--theme-color-text-secondary, #45505f); }
|
|
53
|
+
.prsp-filters-btn-text:hover { background: var(--theme-color-background-tertiary, #eceef2); }
|
|
54
|
+
|
|
55
|
+
/* Slide-out drawer (CSS grid trick: 0fr -> 1fr animates height). */
|
|
56
|
+
.prsp-filters-drawer { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.18s ease; }
|
|
57
|
+
.prsp-filters.drawer-open .prsp-filters-drawer { grid-template-rows: 1fr; }
|
|
58
|
+
.prsp-filters-drawer-inner { overflow: hidden; min-height: 0; }
|
|
59
|
+
.prsp-filters.drawer-open .prsp-filters-drawer-inner { margin-top: 0.6rem; padding: 0.95rem 1.1rem;
|
|
60
|
+
border: 1px solid var(--theme-color-border-light, #e8ebf0); border-radius: 10px; background: var(--theme-color-background-panel, #fff); }
|
|
61
|
+
.prsp-filters-add { margin: 0.4rem 0 0.2rem; }
|
|
62
|
+
/* Drawer footer: filter experience on the left, Clear/Reset/Apply on the right. */
|
|
63
|
+
.prsp-filters-footer { display: flex; align-items: flex-end; justify-content: space-between; gap: 1.5rem; flex-wrap: wrap;
|
|
64
|
+
margin-top: 0.85rem; padding-top: 0.75rem; border-top: 1px solid var(--theme-color-border-light, #e8ebf0); }
|
|
65
|
+
.prsp-filters-experiences { flex: 0 1 auto; min-width: 0; }
|
|
66
|
+
.prsp-filters-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 0.5rem; }
|
|
67
|
+
`,
|
|
26
68
|
CSSPriority: 500,
|
|
27
69
|
|
|
28
70
|
Templates:
|
|
29
71
|
[
|
|
30
72
|
{
|
|
73
|
+
// One coherent control: a search row (search input + a filters icon button that
|
|
74
|
+
// shows the active-filter count and toggles the drawer + a Search button), and a
|
|
75
|
+
// slide-out drawer beneath it holding the filter clauses, "add filter", the saved
|
|
76
|
+
// Filter Experiences dropdown, and Clear / Reset / Apply. Search, Apply and Enter
|
|
77
|
+
// all submit the form -> handleSearch (apply search + current filters).
|
|
31
78
|
Hash: 'PRSP-SUBSET-Filters-Template',
|
|
32
79
|
Template: /*html*/`
|
|
33
80
|
<!-- DefaultPackage pict view template: [PRSP-SUBSET-Filters-Template] -->
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{~
|
|
81
|
+
<div class="prsp-filters" id="PRSP_FilterBar">
|
|
82
|
+
<form id="PRSP_Filter_Form" class="prsp-filters-bar" onsubmit="_Pict.views['PRSP-Filters'].handleSearch(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}'); return false;">
|
|
83
|
+
{~T:PRSP-SUBSET-Filters-Template-Input-Fieldset~}
|
|
84
|
+
{~T:PRSP-SUBSET-Filters-Template-Button-Fieldset~}
|
|
85
|
+
</form>
|
|
86
|
+
<div class="prsp-filters-drawer" id="PRSP_Filter_Drawer">
|
|
87
|
+
<div class="prsp-filters-drawer-inner">
|
|
88
|
+
<div id="PRSP_Filter_Instances" class="prsp-filters-clauses">
|
|
89
|
+
{~FIV:Record~}
|
|
90
|
+
</div>
|
|
91
|
+
{~T:PRSP-SUBSET-Filters-Template-AddFilter-Fieldset~}
|
|
92
|
+
<div class="prsp-filters-footer">
|
|
93
|
+
{~T:PRSP-SUBSET-Filters-Template-ManageFilters-Fieldset~}
|
|
94
|
+
{~T:PRSP-SUBSET-Filters-Template-DrawerActions-Fieldset~}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
38
97
|
</div>
|
|
39
|
-
|
|
40
|
-
{~T:PRSP-SUBSET-Filters-Template-AddFilter-Fieldset~}
|
|
41
|
-
{~T:PRSP-SUBSET-Filters-Template-ManageFilters-Fieldset~}
|
|
42
|
-
</form>
|
|
98
|
+
</div>
|
|
43
99
|
<!-- DefaultPackage end view template: [PRSP-SUBSET-Filters-Template] -->
|
|
44
100
|
`
|
|
45
101
|
},
|
|
@@ -47,10 +103,10 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
|
|
|
47
103
|
Hash: 'PRSP-SUBSET-Filters-Template-Input-Fieldset',
|
|
48
104
|
Template: /*html*/`
|
|
49
105
|
<!-- DefaultPackage pict view template: [PRSP-SUBSET-Filters-Template-Input-Fieldset] -->
|
|
50
|
-
<
|
|
51
|
-
<
|
|
52
|
-
<input id="search_filter" type="text" name="filter">
|
|
53
|
-
</
|
|
106
|
+
<span class="prsp-filters-search">
|
|
107
|
+
<span class="prsp-filters-search-ic">{~I:Search~}</span>
|
|
108
|
+
<input id="search_filter" class="prsp-filters-input" type="text" name="filter" placeholder="Search…" autocomplete="off" aria-label="Search">
|
|
109
|
+
</span>
|
|
54
110
|
<!-- DefaultPackage end view template: [PRSP-SUBSET-Filters-Template-Input-Fieldset] -->
|
|
55
111
|
`
|
|
56
112
|
},
|
|
@@ -58,11 +114,11 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
|
|
|
58
114
|
Hash: 'PRSP-SUBSET-Filters-Template-Button-Fieldset',
|
|
59
115
|
Template: /*html*/`
|
|
60
116
|
<!-- DefaultPackage pict view template: [PRSP-SUBSET-Filters-Template-Button-Fieldset] -->
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
</
|
|
117
|
+
<button type="button" class="prsp-filters-toggle" id="PRSP_Filter_Toggle" title="Filters" aria-label="Filters" onclick="_Pict.views['PRSP-Filters'].toggleFilterDrawer()">
|
|
118
|
+
<span class="prsp-filters-toggle-ic" id="PRSP_Filter_Icon"></span>
|
|
119
|
+
<span class="prsp-filters-toggle-count" id="PRSP_Filter_Count"></span>
|
|
120
|
+
</button>
|
|
121
|
+
<button type="submit" class="prsp-filters-apply prsp-filters-apply-search" id="PRSP_Filter_Button_Apply">Search</button>
|
|
66
122
|
<!-- DefaultPackage end view template: [PRSP-SUBSET-Filters-Template-Button-Fieldset] -->
|
|
67
123
|
`
|
|
68
124
|
},
|
|
@@ -70,21 +126,32 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
|
|
|
70
126
|
Hash: 'PRSP-SUBSET-Filters-Template-ManageFilters-Fieldset',
|
|
71
127
|
Template: /*html*/`
|
|
72
128
|
<!-- DefaultPackage pict view template: [PRSP-SUBSET-Filters-Template-ManageFilters-Fieldset] -->
|
|
73
|
-
<
|
|
74
|
-
<button type="button" id="PRSP_Filter_Button_Manage" title="Manage saved filter experiences" onclick="_Pict.views['PRSP-Filters'].handleManage(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}')">Manage Filter Experience</button>
|
|
129
|
+
<div class="prsp-filters-experiences">
|
|
75
130
|
<div id="FilterPersistenceView-Container"></div>
|
|
76
|
-
</
|
|
131
|
+
</div>
|
|
77
132
|
<!-- DefaultPackage end view template: [PRSP-SUBSET-Filters-Template-ManageFilters-Fieldset] -->
|
|
133
|
+
`
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
Hash: 'PRSP-SUBSET-Filters-Template-DrawerActions-Fieldset',
|
|
137
|
+
Template: /*html*/`
|
|
138
|
+
<!-- DefaultPackage pict view template: [PRSP-SUBSET-Filters-Template-DrawerActions-Fieldset] -->
|
|
139
|
+
<div class="prsp-filters-actions">
|
|
140
|
+
<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>
|
|
141
|
+
<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>
|
|
142
|
+
<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>
|
|
143
|
+
</div>
|
|
144
|
+
<!-- DefaultPackage end view template: [PRSP-SUBSET-Filters-Template-DrawerActions-Fieldset] -->
|
|
78
145
|
`
|
|
79
146
|
},
|
|
80
147
|
{
|
|
81
148
|
Hash: 'PRSP-SUBSET-Filters-Template-AddFilter-Fieldset',
|
|
82
149
|
Template: /*html*/`
|
|
83
150
|
<!-- DefaultPackage pict view template: [PRSP-SUBSET-Filters-Template-AddFilter-Fieldset] -->
|
|
84
|
-
<
|
|
85
|
-
<button type="button" id="PRSP_Filter_Button_Add" title="Add a new filter clause" onclick="_Pict.views['PRSP-Filters'].selectFilterToAdd(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}')"
|
|
151
|
+
<div class="prsp-filters-add">
|
|
152
|
+
<button type="button" class="prsp-filters-btn-text" id="PRSP_Filter_Button_Add" title="Add a new filter clause" onclick="_Pict.views['PRSP-Filters'].selectFilterToAdd(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}')">+ Add filter</button>
|
|
86
153
|
<div id="PRSP-SUBSET-Filters-Template-AddFilter-Dropdown"></div>
|
|
87
|
-
</
|
|
154
|
+
</div>
|
|
88
155
|
<!-- DefaultPackage end view template: [PRSP-SUBSET-Filters-Template-AddFilter-Fieldset] -->
|
|
89
156
|
`
|
|
90
157
|
},
|
|
@@ -236,6 +303,10 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
236
303
|
this.newFilterSearchApplied = false;
|
|
237
304
|
this.addFilterCallback = null;
|
|
238
305
|
this.removeFilterCallback = null;
|
|
306
|
+
// Consolidated filter control state: drawer open/closed, and the last-applied search
|
|
307
|
+
// string per record set (so a search no longer clears the search box on re-render).
|
|
308
|
+
this._drawerOpen = false;
|
|
309
|
+
this._searchString = {};
|
|
239
310
|
// Render-epoch counter, bumped any time the filter list is re-rendered
|
|
240
311
|
// or a new filter experience is applied. Deferred filter post-render
|
|
241
312
|
// work (e.g. the setTimeout-scheduled transaction drain in
|
|
@@ -348,6 +419,67 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
348
419
|
return super.onMarshalToView();
|
|
349
420
|
}
|
|
350
421
|
|
|
422
|
+
/** Toggle the slide-out filter drawer beneath the search bar. */
|
|
423
|
+
toggleFilterDrawer()
|
|
424
|
+
{
|
|
425
|
+
this._drawerOpen = !this._drawerOpen;
|
|
426
|
+
const tmpBar = document.getElementById('PRSP_FilterBar');
|
|
427
|
+
if (tmpBar) { tmpBar.classList.toggle('drawer-open', this._drawerOpen); }
|
|
428
|
+
return this._drawerOpen;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The current search term, read back from the active route URL (the source of truth) so
|
|
433
|
+
* the search box stays populated across re-renders and reflects bookmarked/filtered URLs.
|
|
434
|
+
* performSearch builds `.../FilteredTo/FBVOR~<field>~LK~<encoded %term%>~...` from the
|
|
435
|
+
* SearchFields, so the term is the first LK value in the FilteredTo segment.
|
|
436
|
+
*
|
|
437
|
+
* @return {string}
|
|
438
|
+
*/
|
|
439
|
+
_searchTermFromURL()
|
|
440
|
+
{
|
|
441
|
+
const tmpCurrent = this.pict.providers.PictRouter && this.pict.providers.PictRouter.router && this.pict.providers.PictRouter.router.current
|
|
442
|
+
? this.pict.providers.PictRouter.router.current[0] : null;
|
|
443
|
+
const tmpUrl = (tmpCurrent && (tmpCurrent.url || tmpCurrent.hashString)) || '';
|
|
444
|
+
if (tmpUrl.indexOf('FilteredTo/') < 0) { return ''; }
|
|
445
|
+
const tmpFilteredPart = (tmpUrl.split('FilteredTo/')[1] || '').split('/FilterExperience')[0];
|
|
446
|
+
const tmpMatch = tmpFilteredPart.match(/LK~([^~]+)/);
|
|
447
|
+
if (!tmpMatch) { return ''; }
|
|
448
|
+
let tmpValue = tmpMatch[1];
|
|
449
|
+
try { tmpValue = decodeURIComponent(tmpValue); } catch (pError) { /* leave raw */ }
|
|
450
|
+
return tmpValue.replace(/^%+|%+$/g, '');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** The number of active (structured) filter clauses for a record set. */
|
|
454
|
+
getActiveFilterCount(pRecordSet)
|
|
455
|
+
{
|
|
456
|
+
const tmpClauses = this.pict && this.pict.Bundle && this.pict.Bundle._ActiveFilterState && this.pict.Bundle._ActiveFilterState[pRecordSet]
|
|
457
|
+
? this.pict.Bundle._ActiveFilterState[pRecordSet].FilterClauses : null;
|
|
458
|
+
return Array.isArray(tmpClauses) ? tmpClauses.length : 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Repaint the filter-bar chrome after a (re)render: the filters icon (outline vs filled
|
|
463
|
+
* + count badge), the active-filter highlight, the persisted drawer-open state, and the
|
|
464
|
+
* search input value (so applying a search no longer clears the search box).
|
|
465
|
+
*
|
|
466
|
+
* @param {string} pRecordSet
|
|
467
|
+
*/
|
|
468
|
+
_paintFilterControls(pRecordSet)
|
|
469
|
+
{
|
|
470
|
+
const tmpBar = document.getElementById('PRSP_FilterBar');
|
|
471
|
+
if (!tmpBar) { return; }
|
|
472
|
+
const tmpCount = this.getActiveFilterCount(pRecordSet);
|
|
473
|
+
tmpBar.classList.toggle('has-filters', tmpCount > 0);
|
|
474
|
+
tmpBar.classList.toggle('drawer-open', !!this._drawerOpen);
|
|
475
|
+
const tmpIcon = document.getElementById('PRSP_Filter_Icon');
|
|
476
|
+
if (tmpIcon) { tmpIcon.innerHTML = (tmpCount > 0) ? ViewRecordSetSUBSETFilters.FilterIconFilled : ViewRecordSetSUBSETFilters.FilterIconOutline; }
|
|
477
|
+
const tmpCountEl = document.getElementById('PRSP_Filter_Count');
|
|
478
|
+
if (tmpCountEl) { tmpCountEl.textContent = (tmpCount > 0) ? String(tmpCount) : ''; }
|
|
479
|
+
const tmpInput = document.getElementById('search_filter');
|
|
480
|
+
if (tmpInput) { tmpInput.value = this._searchTermFromURL(); }
|
|
481
|
+
}
|
|
482
|
+
|
|
351
483
|
/**
|
|
352
484
|
* @param {Event} pEvent - The DOM event that triggered the search
|
|
353
485
|
* @param {string} pRecordSet - The record set being filtered
|
|
@@ -355,12 +487,12 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
355
487
|
*/
|
|
356
488
|
handleSearch(pEvent, pRecordSet, pViewContext)
|
|
357
489
|
{
|
|
358
|
-
pEvent.preventDefault(); // don't submit the form
|
|
359
|
-
pEvent.stopPropagation();
|
|
490
|
+
if (pEvent) { pEvent.preventDefault(); pEvent.stopPropagation(); } // don't submit the form
|
|
360
491
|
this.newFilterSearchApplied = true;
|
|
361
|
-
//FIXME: store this filter string in the bundle so we can re-apply it on re-render
|
|
362
492
|
const tmpSearchString = this.pict.ContentAssignment.readContent(`input[name="filter"]`);
|
|
363
|
-
|
|
493
|
+
// Remember the applied search so the re-render below doesn't blank the search box.
|
|
494
|
+
this._searchString[pRecordSet] = tmpSearchString ? String(tmpSearchString) : '';
|
|
495
|
+
this.performSearch(pRecordSet, pViewContext, this._searchString[pRecordSet]);
|
|
364
496
|
}
|
|
365
497
|
|
|
366
498
|
/**
|
|
@@ -433,6 +565,7 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
433
565
|
if (pEvent) pEvent.preventDefault();
|
|
434
566
|
this.bumpRenderEpoch();
|
|
435
567
|
this.pict.ContentAssignment.assignContent('input[name="filter"]', '');
|
|
568
|
+
this._searchString[pRecordSet] = '';
|
|
436
569
|
this.pict.Bundle._ActiveFilterState[pRecordSet].FilterClauses = [];
|
|
437
570
|
this.pict.providers.FilterDataProvider.removeDefaultFilterExperience(pRecordSet, pViewContext);
|
|
438
571
|
this.performSearch(pRecordSet, pViewContext, '');
|
|
@@ -538,6 +671,26 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
538
671
|
return Object.values(tmpRecordsetProvider.getFilterSchema()).flatMap((pFilter) => pFilter.AvailableClauses || []);
|
|
539
672
|
}
|
|
540
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Lifecycle hook that triggers before the view is rendered. The consolidated filter
|
|
676
|
+
* control's markup is owned by this module now, so re-assert its templates here — this
|
|
677
|
+
* neutralizes any host/server template override that previously replaced the filter UI,
|
|
678
|
+
* letting apps brand the control via CSS (theme tokens) rather than by swapping markup.
|
|
679
|
+
*
|
|
680
|
+
* @param {import('pict-view').Renderable} pRenderable - The renderable about to be rendered.
|
|
681
|
+
*/
|
|
682
|
+
onBeforeRender(pRenderable)
|
|
683
|
+
{
|
|
684
|
+
if (this.pict.TemplateProvider && Array.isArray(_DEFAULT_CONFIGURATION_SUBSET_Filter.Templates))
|
|
685
|
+
{
|
|
686
|
+
for (const tmpTemplate of _DEFAULT_CONFIGURATION_SUBSET_Filter.Templates)
|
|
687
|
+
{
|
|
688
|
+
this.pict.TemplateProvider.addTemplate(tmpTemplate.Hash, tmpTemplate.Template);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return super.onBeforeRender(pRenderable);
|
|
692
|
+
}
|
|
693
|
+
|
|
541
694
|
/**
|
|
542
695
|
* Lifecycle hook that triggers after the view is rendered.
|
|
543
696
|
* @param {import('pict-view').Renderable} pRenderable - The renderable that was rendered.
|
|
@@ -584,6 +737,32 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
584
737
|
this.pict.providers.FilterDataProvider.applyExpectedFilterExperience(tmpRecordSet, tmpViewContext);
|
|
585
738
|
}
|
|
586
739
|
|
|
740
|
+
// Consolidated filter control: whenever the search row is in the DOM, refresh the
|
|
741
|
+
// search box, the filters icon/count + drawer state, and surface the saved Filter
|
|
742
|
+
// Experiences dropdown in the drawer. (Guarded on the DOM element rather than the
|
|
743
|
+
// renderable hash because the list embeds this view via {~FV:~}.)
|
|
744
|
+
if (document.getElementById('PRSP_Filter_Icon'))
|
|
745
|
+
{
|
|
746
|
+
let tmpFilterRecordSet = tmpRecordSet;
|
|
747
|
+
let tmpFilterViewContext = tmpViewContext || 'List';
|
|
748
|
+
if (!tmpFilterRecordSet && this.pict.Bundle && this.pict.Bundle._ActiveFilterState)
|
|
749
|
+
{
|
|
750
|
+
tmpFilterRecordSet = Object.keys(this.pict.Bundle._ActiveFilterState)[0];
|
|
751
|
+
}
|
|
752
|
+
if (tmpFilterRecordSet)
|
|
753
|
+
{
|
|
754
|
+
this._paintFilterControls(tmpFilterRecordSet);
|
|
755
|
+
// (Re)render the experiences dropdown only when its container is empty — i.e. on
|
|
756
|
+
// a fresh filter render — not on every sub-render (add-filter dropdown, etc.).
|
|
757
|
+
const tmpExpContainer = document.getElementById('FilterPersistenceView-Container');
|
|
758
|
+
if (this.pict.views.FilterPersistenceView && tmpExpContainer && !tmpExpContainer.querySelector('#FilterPersistenceView-Content'))
|
|
759
|
+
{
|
|
760
|
+
this.pict.views.FilterPersistenceView.initializeFilterPersistenceViewUI(tmpFilterRecordSet, tmpFilterViewContext);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (this.pict.CSSMap && typeof this.pict.CSSMap.injectCSS === 'function') { this.pict.CSSMap.injectCSS(); }
|
|
764
|
+
}
|
|
765
|
+
|
|
587
766
|
return res;
|
|
588
767
|
}
|
|
589
768
|
|
|
@@ -730,6 +909,11 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
730
909
|
};
|
|
731
910
|
}
|
|
732
911
|
|
|
912
|
+
// Funnel icons for the filters toggle — outline when no filters are set, filled when set.
|
|
913
|
+
// currentColor so they follow the (themeable) toggle text color.
|
|
914
|
+
ViewRecordSetSUBSETFilters.FilterIconOutline = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 5.5h17l-6.6 7.8v4.7l-3.8 2v-6.7z"/></svg>';
|
|
915
|
+
ViewRecordSetSUBSETFilters.FilterIconFilled = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3.5 5.5h17l-6.6 7.8v4.7l-3.8 2v-6.7z"/></svg>';
|
|
916
|
+
|
|
733
917
|
module.exports = ViewRecordSetSUBSETFilters;
|
|
734
918
|
|
|
735
919
|
module.exports.default_configuration = _DEFAULT_CONFIGURATION_SUBSET_Filter;
|