pict-section-recordset 1.0.70 → 1.2.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 (69) hide show
  1. package/package.json +5 -1
  2. package/source/providers/Filter-Data-Provider.js +16 -5
  3. package/source/views/Filter-PersistenceView.js +150 -35
  4. package/source/views/RecordSet-Filters.js +230 -28
  5. package/source/views/filters/RecordSet-Filter-Base.js +86 -8
  6. package/source/views/read/RecordSet-Read.js +308 -2
  7. package/types/providers/Filter-Data-Provider.d.ts +1 -1
  8. package/types/providers/Filter-Data-Provider.d.ts.map +1 -1
  9. package/types/views/Filter-PersistenceView.d.ts +23 -2
  10. package/types/views/Filter-PersistenceView.d.ts.map +1 -1
  11. package/types/views/RecordSet-Filters.d.ts +26 -1
  12. package/types/views/RecordSet-Filters.d.ts.map +1 -1
  13. package/types/views/filters/RecordSet-Filter-Base.d.ts +14 -0
  14. package/types/views/filters/RecordSet-Filter-Base.d.ts.map +1 -1
  15. package/types/views/list/RecordSet-List.d.ts.map +1 -1
  16. package/types/views/read/RecordSet-Read.d.ts +51 -0
  17. package/types/views/read/RecordSet-Read.d.ts.map +1 -1
  18. package/.vscode/launch.json +0 -46
  19. package/CONTRIBUTING.md +0 -50
  20. package/debug/Harness.js +0 -0
  21. package/docs/.nojekyll +0 -0
  22. package/docs/README.md +0 -76
  23. package/docs/_brand.json +0 -18
  24. package/docs/_cover.md +0 -11
  25. package/docs/_sidebar.md +0 -19
  26. package/docs/_version.json +0 -7
  27. package/docs/api-reference.md +0 -233
  28. package/docs/filters.md +0 -151
  29. package/docs/index.html +0 -38
  30. package/docs/record-providers.md +0 -155
  31. package/docs/retold-catalog.json +0 -87
  32. package/docs/retold-keyword-index.json +0 -5227
  33. package/docs/views/create/README.md +0 -181
  34. package/docs/views/dashboard/README.md +0 -308
  35. package/docs/views/list/README.md +0 -260
  36. package/docs/views/read/README.md +0 -216
  37. package/eslint.config.mjs +0 -10
  38. package/example_applications/README.md +0 -39
  39. package/example_applications/ServeExamples.js +0 -82
  40. package/example_applications/bookstore/.quackage.json +0 -9
  41. package/example_applications/bookstore/Bookstore-Application-Configuration.json +0 -4
  42. package/example_applications/bookstore/Bookstore-Application.js +0 -671
  43. package/example_applications/bookstore/css/bookstore.css +0 -729
  44. package/example_applications/bookstore/css/pure.min.css +0 -11
  45. package/example_applications/bookstore/html/index.html +0 -46
  46. package/example_applications/bookstore/package.json +0 -34
  47. package/example_applications/bookstore/providers/PictRouter-Bookstore.json +0 -32
  48. package/example_applications/bookstore/views/PictView-Bookstore-Content-About.json +0 -21
  49. package/example_applications/bookstore/views/PictView-Bookstore-Content-Legal.json +0 -21
  50. package/example_applications/bookstore/views/PictView-Bookstore-Dashboard.js +0 -147
  51. package/example_applications/bookstore/views/PictView-Bookstore-Layout.js +0 -85
  52. package/example_applications/bookstore/views/PictView-Bookstore-Login.js +0 -58
  53. package/example_applications/bookstore/views/PictView-Bookstore-Navigation.js +0 -228
  54. package/example_applications/index.html +0 -50
  55. package/example_applications/mocks/book-edit-view.html +0 -173
  56. package/example_applications/mocks/book-read-view.html +0 -166
  57. package/example_applications/mocks/list-view.html +0 -185
  58. package/example_applications/package.json +0 -16
  59. package/example_applications/simple_entity/.quackage.json +0 -9
  60. package/example_applications/simple_entity/README-Simple-RecordSet.md +0 -8
  61. package/example_applications/simple_entity/Simple-RecordSet-Application.js +0 -887
  62. package/example_applications/simple_entity/html/index.html +0 -207
  63. package/example_applications/simple_entity/package.json +0 -27
  64. package/test/PictSectionRecordSet-Basic_tests.js +0 -205
  65. package/test/PictSectionRecordSet-Filter-Data-Provider_tests.js +0 -263
  66. package/test/PictSectionRecordSet-Filter-InstanceViews-Render_tests.js +0 -328
  67. package/test/PictSectionRecordSet-RecordProvider-Meadow_tests.js +0 -216
  68. package/tsconfig.build.json +0 -16
  69. package/tsconfig.json +0 -16
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "pict-section-recordset",
3
- "version": "1.0.70",
3
+ "version": "1.2.0",
4
4
  "description": "Pict dynamic record set management views",
5
5
  "main": "source/Pict-Section-RecordSet.js",
6
+ "files": [
7
+ "source",
8
+ "types"
9
+ ],
6
10
  "directories": {
7
11
  "test": "test"
8
12
  },
@@ -128,10 +128,17 @@ class FilterDataProvider extends libPictProvider
128
128
  */
129
129
  navigateToFilterExperienceRoute(tmpFilterExperience, pRecordSet, pViewContext)
130
130
  {
131
- // go to the new url with the filter experience encoded param
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 = this.getCurrentFilterName({ FilterClauses: activeFilterExperienceClauses }, pRecordSet, pViewContext);
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: false,
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
- <!-- Content for Filter Persistence View goes here -->
35
- <div id="FilterPersistenceView-Header">
36
- <h3>Filter Experience Settings</h3>
37
- </div>
38
- <div id="FilterPersistenceView-Body">
39
- <div class="FilterPersistenceView-ActiveSettings">
40
- <label for="CurrentFilterName">Current Filter Experience:</label>
41
- <span id="FilterPersistenceView-CurrentFilterNameInput-ValidationMessage" style="color: red; font-size: 0.9em; margin-left: 10px;"></span>
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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: false,
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
- <form id="PRSP_Filter_Form" onsubmit="_Pict.views['PRSP-Filters'].handleSearch(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}'); return false;">
35
- {~T:PRSP-SUBSET-Filters-Template-Input-Fieldset~}
36
- <div id="PRSP_Filter_Instances">
37
- {~FIV:Record~}
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
- {~T:PRSP-SUBSET-Filters-Template-Button-Fieldset~}
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
- <fieldset>
51
- <label for="search_filter">Filter:</label>
52
- <input id="search_filter" type="text" name="filter">
53
- </fieldset>
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
- <fieldset>
62
- <button type="button" 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>
63
- <button type="button" 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>
64
- <button type="submit" id="PRSP_Filter_Button_Apply">Apply</button>
65
- </fieldset>
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
- <fieldset>
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
- </fieldset>
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
- <fieldset>
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~}')">+</button>
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
- </fieldset>
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
- this.performSearch(pRecordSet, pViewContext, tmpSearchString ? String(tmpSearchString) : '');
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,44 @@ 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
+ // Re-assert ONLY the consolidated-control chrome (the wrapper, search row, filter toggle +
685
+ // Search button, the Filter Experiences container, and Clear/Reset/Apply) so the module owns
686
+ // that markup. Deliberately NOT the add-filter dropdown or per-clause templates: host apps
687
+ // (e.g. Headlight) layer their own styled add-filter popover and clause UI on top, and
688
+ // re-asserting those clobbers them — re-exposing bare native <select> dropdowns and a dead
689
+ // remove button. Those host overrides register at init and must survive each re-render.
690
+ const tmpChromeTemplateHashes =
691
+ [
692
+ 'PRSP-SUBSET-Filters-Template',
693
+ 'PRSP-SUBSET-Filters-Template-Input-Fieldset',
694
+ 'PRSP-SUBSET-Filters-Template-Button-Fieldset',
695
+ 'PRSP-SUBSET-Filters-Template-ManageFilters-Fieldset',
696
+ 'PRSP-SUBSET-Filters-Template-DrawerActions-Fieldset'
697
+ ];
698
+ if (this.pict.TemplateProvider && Array.isArray(_DEFAULT_CONFIGURATION_SUBSET_Filter.Templates))
699
+ {
700
+ for (const tmpTemplateHash of tmpChromeTemplateHashes)
701
+ {
702
+ const tmpTemplate = _DEFAULT_CONFIGURATION_SUBSET_Filter.Templates.find((pTemplate) => pTemplate.Hash === tmpTemplateHash);
703
+ if (tmpTemplate)
704
+ {
705
+ this.pict.TemplateProvider.addTemplate(tmpTemplate.Hash, tmpTemplate.Template);
706
+ }
707
+ }
708
+ }
709
+ return super.onBeforeRender(pRenderable);
710
+ }
711
+
541
712
  /**
542
713
  * Lifecycle hook that triggers after the view is rendered.
543
714
  * @param {import('pict-view').Renderable} pRenderable - The renderable that was rendered.
@@ -584,6 +755,32 @@ class ViewRecordSetSUBSETFilters extends libPictView
584
755
  this.pict.providers.FilterDataProvider.applyExpectedFilterExperience(tmpRecordSet, tmpViewContext);
585
756
  }
586
757
 
758
+ // Consolidated filter control: whenever the search row is in the DOM, refresh the
759
+ // search box, the filters icon/count + drawer state, and surface the saved Filter
760
+ // Experiences dropdown in the drawer. (Guarded on the DOM element rather than the
761
+ // renderable hash because the list embeds this view via {~FV:~}.)
762
+ if (document.getElementById('PRSP_Filter_Icon'))
763
+ {
764
+ let tmpFilterRecordSet = tmpRecordSet;
765
+ let tmpFilterViewContext = tmpViewContext || 'List';
766
+ if (!tmpFilterRecordSet && this.pict.Bundle && this.pict.Bundle._ActiveFilterState)
767
+ {
768
+ tmpFilterRecordSet = Object.keys(this.pict.Bundle._ActiveFilterState)[0];
769
+ }
770
+ if (tmpFilterRecordSet)
771
+ {
772
+ this._paintFilterControls(tmpFilterRecordSet);
773
+ // (Re)render the experiences dropdown only when its container is empty — i.e. on
774
+ // a fresh filter render — not on every sub-render (add-filter dropdown, etc.).
775
+ const tmpExpContainer = document.getElementById('FilterPersistenceView-Container');
776
+ if (this.pict.views.FilterPersistenceView && tmpExpContainer && !tmpExpContainer.querySelector('#FilterPersistenceView-Content'))
777
+ {
778
+ this.pict.views.FilterPersistenceView.initializeFilterPersistenceViewUI(tmpFilterRecordSet, tmpFilterViewContext);
779
+ }
780
+ }
781
+ if (this.pict.CSSMap && typeof this.pict.CSSMap.injectCSS === 'function') { this.pict.CSSMap.injectCSS(); }
782
+ }
783
+
587
784
  return res;
588
785
  }
589
786
 
@@ -730,6 +927,11 @@ class ViewRecordSetSUBSETFilters extends libPictView
730
927
  };
731
928
  }
732
929
 
930
+ // Funnel icons for the filters toggle — outline when no filters are set, filled when set.
931
+ // currentColor so they follow the (themeable) toggle text color.
932
+ 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>';
933
+ 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>';
934
+
733
935
  module.exports = ViewRecordSetSUBSETFilters;
734
936
 
735
937
  module.exports.default_configuration = _DEFAULT_CONFIGURATION_SUBSET_Filter;