pict-section-recordset 1.1.0 → 1.3.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 (67) hide show
  1. package/package.json +5 -1
  2. package/source/views/RecordSet-Filters.js +196 -72
  3. package/source/views/filters/RecordSet-Filter-Base.js +86 -8
  4. package/source/views/read/RecordSet-Read.js +308 -2
  5. package/types/providers/Filter-Data-Provider.d.ts +1 -1
  6. package/types/providers/Filter-Data-Provider.d.ts.map +1 -1
  7. package/types/views/Filter-PersistenceView.d.ts +23 -2
  8. package/types/views/Filter-PersistenceView.d.ts.map +1 -1
  9. package/types/views/RecordSet-Filters.d.ts +54 -2
  10. package/types/views/RecordSet-Filters.d.ts.map +1 -1
  11. package/types/views/filters/RecordSet-Filter-Base.d.ts +14 -0
  12. package/types/views/filters/RecordSet-Filter-Base.d.ts.map +1 -1
  13. package/types/views/list/RecordSet-List.d.ts.map +1 -1
  14. package/types/views/read/RecordSet-Read.d.ts +51 -0
  15. package/types/views/read/RecordSet-Read.d.ts.map +1 -1
  16. package/.vscode/launch.json +0 -46
  17. package/CONTRIBUTING.md +0 -50
  18. package/debug/Harness.js +0 -0
  19. package/docs/.nojekyll +0 -0
  20. package/docs/README.md +0 -76
  21. package/docs/_brand.json +0 -18
  22. package/docs/_cover.md +0 -11
  23. package/docs/_sidebar.md +0 -19
  24. package/docs/_version.json +0 -7
  25. package/docs/api-reference.md +0 -233
  26. package/docs/filters.md +0 -151
  27. package/docs/index.html +0 -38
  28. package/docs/record-providers.md +0 -155
  29. package/docs/retold-catalog.json +0 -87
  30. package/docs/retold-keyword-index.json +0 -5227
  31. package/docs/views/create/README.md +0 -181
  32. package/docs/views/dashboard/README.md +0 -308
  33. package/docs/views/list/README.md +0 -260
  34. package/docs/views/read/README.md +0 -216
  35. package/eslint.config.mjs +0 -10
  36. package/example_applications/README.md +0 -39
  37. package/example_applications/ServeExamples.js +0 -82
  38. package/example_applications/bookstore/.quackage.json +0 -9
  39. package/example_applications/bookstore/Bookstore-Application-Configuration.json +0 -4
  40. package/example_applications/bookstore/Bookstore-Application.js +0 -671
  41. package/example_applications/bookstore/css/bookstore.css +0 -729
  42. package/example_applications/bookstore/css/pure.min.css +0 -11
  43. package/example_applications/bookstore/html/index.html +0 -46
  44. package/example_applications/bookstore/package.json +0 -34
  45. package/example_applications/bookstore/providers/PictRouter-Bookstore.json +0 -32
  46. package/example_applications/bookstore/views/PictView-Bookstore-Content-About.json +0 -21
  47. package/example_applications/bookstore/views/PictView-Bookstore-Content-Legal.json +0 -21
  48. package/example_applications/bookstore/views/PictView-Bookstore-Dashboard.js +0 -147
  49. package/example_applications/bookstore/views/PictView-Bookstore-Layout.js +0 -85
  50. package/example_applications/bookstore/views/PictView-Bookstore-Login.js +0 -58
  51. package/example_applications/bookstore/views/PictView-Bookstore-Navigation.js +0 -228
  52. package/example_applications/index.html +0 -50
  53. package/example_applications/mocks/book-edit-view.html +0 -173
  54. package/example_applications/mocks/book-read-view.html +0 -166
  55. package/example_applications/mocks/list-view.html +0 -185
  56. package/example_applications/package.json +0 -16
  57. package/example_applications/simple_entity/.quackage.json +0 -9
  58. package/example_applications/simple_entity/README-Simple-RecordSet.md +0 -8
  59. package/example_applications/simple_entity/Simple-RecordSet-Application.js +0 -887
  60. package/example_applications/simple_entity/html/index.html +0 -207
  61. package/example_applications/simple_entity/package.json +0 -27
  62. package/test/PictSectionRecordSet-Basic_tests.js +0 -205
  63. package/test/PictSectionRecordSet-Filter-Data-Provider_tests.js +0 -263
  64. package/test/PictSectionRecordSet-Filter-InstanceViews-Render_tests.js +0 -328
  65. package/test/PictSectionRecordSet-RecordProvider-Meadow_tests.js +0 -216
  66. package/tsconfig.build.json +0 -16
  67. package/tsconfig.json +0 -16
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "pict-section-recordset",
3
- "version": "1.1.0",
3
+ "version": "1.3.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
  },
@@ -58,12 +58,36 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
58
58
  .prsp-filters-drawer-inner { overflow: hidden; min-height: 0; }
59
59
  .prsp-filters.drawer-open .prsp-filters-drawer-inner { margin-top: 0.6rem; padding: 0.95rem 1.1rem;
60
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; }
61
+ .prsp-filters-add { position: relative; margin: 0.4rem 0 0.2rem; }
62
+ .prsp-addfilter-trigger { display: inline-flex; align-items: center; gap: 0.35rem; }
62
63
  /* Drawer footer: filter experience on the left, Clear/Reset/Apply on the right. */
63
64
  .prsp-filters-footer { display: flex; align-items: flex-end; justify-content: space-between; gap: 1.5rem; flex-wrap: wrap;
64
65
  margin-top: 0.85rem; padding-top: 0.75rem; border-top: 1px solid var(--theme-color-border-light, #e8ebf0); }
65
66
  .prsp-filters-experiences { flex: 0 1 auto; min-width: 0; }
66
67
  .prsp-filters-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 0.5rem; }
68
+
69
+ /* Module-owned "Add filter" popover (replaces the old native <select> pickers). */
70
+ .prsp-addfilter-pop { position: absolute; z-index: 30; top: calc(100% + 0.35rem); left: 0; min-width: 250px; max-width: 340px; display: none; }
71
+ .prsp-addfilter-pop.open { display: block; }
72
+ .prsp-addfilter-panel { background: var(--theme-color-background-panel, #fff); border: 1px solid var(--theme-color-border-default, #d7dce3);
73
+ border-radius: 10px; box-shadow: 0 10px 28px rgba(17, 24, 39, 0.14); overflow: hidden; }
74
+ .prsp-addfilter-search { display: flex; align-items: center; gap: 0.4rem; padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--theme-color-border-light, #e8ebf0); }
75
+ .prsp-addfilter-search-ic { display: inline-flex; color: var(--theme-color-text-muted, #6b7686); font-size: 0.9rem; }
76
+ .prsp-addfilter-search input { flex: 1 1 auto; min-width: 0; font: inherit; font-size: 0.9rem; border: none; outline: none; background: transparent; color: var(--theme-color-text-primary, #1f2733); }
77
+ .prsp-addfilter-list { max-height: 280px; overflow-y: auto; }
78
+ .prsp-addfilter-empty { padding: 0.7rem 0.8rem; color: var(--theme-color-text-muted, #6b7686); font-size: 0.86rem; }
79
+ .prsp-addfilter-field { border-bottom: 1px solid var(--theme-color-border-light, #eef1f5); }
80
+ .prsp-addfilter-field:last-child { border-bottom: none; }
81
+ .prsp-addfilter-field-btn { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; width: 100%;
82
+ font: inherit; font-size: 0.9rem; text-align: left; cursor: pointer; padding: 0.5rem 0.75rem; border: none; background: transparent; color: var(--theme-color-text-primary, #1f2733); }
83
+ .prsp-addfilter-field-btn:hover { background: var(--theme-color-background-tertiary, #eceef2); }
84
+ .prsp-addfilter-chev { display: inline-flex; flex: 0 0 auto; color: var(--theme-color-text-muted, #6b7686); font-size: 0.85rem; transition: transform 0.15s ease; }
85
+ .prsp-addfilter-field.is-expanded .prsp-addfilter-chev { transform: rotate(90deg); }
86
+ .prsp-addfilter-clauses { display: flex; flex-direction: column; }
87
+ .prsp-addfilter-clause { display: flex; align-items: center; gap: 0.4rem; width: 100%; text-align: left; cursor: pointer;
88
+ font: inherit; font-size: 0.85rem; padding: 0.4rem 0.8rem 0.4rem 1.6rem; border: none; background: transparent; color: var(--theme-color-text-secondary, #45505f); }
89
+ .prsp-addfilter-clause:hover { background: color-mix(in srgb, var(--theme-color-brand-primary, #156dd1) 10%, transparent); color: var(--theme-color-brand-primary, #156dd1); }
90
+ .prsp-addfilter-clause-ic { display: inline-flex; font-size: 0.8rem; }
67
91
  `,
68
92
  CSSPriority: 500,
69
93
 
@@ -149,54 +173,62 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
149
173
  Template: /*html*/`
150
174
  <!-- DefaultPackage pict view template: [PRSP-SUBSET-Filters-Template-AddFilter-Fieldset] -->
151
175
  <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>
153
- <div id="PRSP-SUBSET-Filters-Template-AddFilter-Dropdown"></div>
176
+ <button type="button" class="prsp-filters-btn-text prsp-addfilter-trigger" id="PRSP_Filter_Button_Add" title="Add a new filter clause" onclick="_Pict.views['PRSP-Filters'].toggleAddFilterPopover(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}')">{~I:Plus~} Add filter</button>
177
+ <div class="prsp-addfilter-pop" id="PRSP_AddFilter_Popover"></div>
154
178
  </div>
155
179
  <!-- DefaultPackage end view template: [PRSP-SUBSET-Filters-Template-AddFilter-Fieldset] -->
156
180
  `
157
181
  },
158
182
  {
159
- Hash: 'PRSP-SUBSET-Filters-Template-AddFilter-Dropdown',
183
+ Hash: 'PRSP-AddFilter-Popover',
160
184
  Template: /*html*/`
161
- <!-- DefaultPackage pict view template: [PRSP-SUBSET-Filters-Template-AddFilter-Dropdown] -->
162
- <div>
163
- <select id="PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-Select" data-i-view-context="{~D:Record.ViewContext~}" onchange="event.preventDefault(); _Pict.views['PRSP-Filters'].render('PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-AddFilterClauseDropdown', undefined,
164
- {
165
- ViewContext: '{~D:Record.ViewContext~}',
166
- RecordSet: event.target.querySelector('option:checked').getAttribute('data-i-recordset'),
167
- FilterKey: event.target.querySelector('option:checked').getAttribute('data-i-filter-key'),
168
- AvailableClauses: _Pict.providers[\`RSP-Provider-\${event.target.querySelector('option:checked').getAttribute('data-i-recordset')}\`].getFilterClauseSchemaForKey(event.target.querySelector('option:checked').getAttribute('data-i-filter-key')).AvailableClauses,
169
- });">
170
- {~TS:PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-Entry:Scope.getFilterSchema()~}
171
- </select>
172
- <div id="PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-AddFilterClauseDropdown">
185
+ <!-- DefaultPackage pict view template: [PRSP-AddFilter-Popover] -->
186
+ <div class="prsp-addfilter-panel">
187
+ <div class="prsp-addfilter-search">
188
+ <span class="prsp-addfilter-search-ic">{~I:Search~}</span>
189
+ <input type="text" id="PRSP_AddFilter_Search" placeholder="Search filters…" autocomplete="off" value="{~D:AppData.PRSPAddFilter.Search~}" oninput="_Pict.views['PRSP-Filters'].searchAddFilter(this.value)" onkeydown="if (event.key === 'Escape') { event.preventDefault(); _Pict.views['PRSP-Filters'].closeAddFilterPopover(); }">
190
+ </div>
191
+ <div class="prsp-addfilter-list" id="PRSP_AddFilter_List">
192
+ {~T:PRSP-AddFilter-List~}
173
193
  </div>
174
194
  </div>
175
- <!-- DefaultPackage end view template: [PRSP-SUBSET-Filters-Template-AddFilter-Dropdown] -->
195
+ <!-- DefaultPackage end view template: [PRSP-AddFilter-Popover] -->
176
196
  `
177
197
  },
178
198
  {
179
- Hash: 'PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-AddFilterClauseDropdown',
199
+ Hash: 'PRSP-AddFilter-List',
180
200
  Template: /*html*/`
181
- <!-- DefaultPackage pict view template: [PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-AddFilterClauseDropdown] -->
182
- <select id="PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-AddFilterClauseDropdown-Select">
183
- {~TS:PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-Entry:Record.AvailableClauses~}
184
- </select>
185
- <button type="button" id="PRSP_Filter_Button_ConfirmAdd" onclick="_Pict.views['PRSP-Filters'].addFilter(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}',
186
- document.getElementById('PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-AddFilterClauseDropdown').querySelector('option:checked').getAttribute('data-i-filter-key'),
187
- document.getElementById('PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-AddFilterClauseDropdown').querySelector('option:checked').getAttribute('data-i-clause-key'),
188
- )">Add Filter</button>
189
- <!-- DefaultPackage end view template: [PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-AddFilterClauseDropdown] -->
201
+ <!-- DefaultPackage pict view template: [PRSP-AddFilter-List] -->
202
+ {~TS:PRSP-AddFilter-Field:AppData.PRSPAddFilter.Fields~}
203
+ {~NE:AppData.PRSPAddFilter.IsEmpty^<div class="prsp-addfilter-empty">No filters found.</div>~}
204
+ <!-- DefaultPackage end view template: [PRSP-AddFilter-List] -->
190
205
  `
191
206
  },
192
207
  {
193
- Hash: 'PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-Entry',
208
+ Hash: 'PRSP-AddFilter-Field',
194
209
  Template: /*html*/`
195
- <!-- DefaultPackage pict view template: [PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-Entry] -->
196
- <option value="{~D:Record.FilterKey~}|{~D:Record.ClauseKey~}" data-i-recordset="{~D:Record.RecordSet~}" data-i-filter-key="{~D:Record.FilterKey~}" data-i-clause-key="{~D:Record.ClauseKey~}">
197
- {~D:Record.DisplayName~}
198
- </option>
199
- <!-- DefaultPackage end view template: [PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-Entry] -->
210
+ <!-- DefaultPackage pict view template: [PRSP-AddFilter-Field] -->
211
+ <div class="prsp-addfilter-field {~D:Record.ExpandedClass~}">
212
+ <button type="button" class="prsp-addfilter-field-btn" onclick="_Pict.views['PRSP-Filters'].toggleAddFilterField('{~D:Record.FilterKey~}')">
213
+ <span class="prsp-addfilter-field-name">{~D:Record.DisplayName~}</span>
214
+ <span class="prsp-addfilter-chev">{~I:ChevronRight~}</span>
215
+ </button>
216
+ <div class="prsp-addfilter-clauses">
217
+ {~TS:PRSP-AddFilter-Clause:Record.ClausesToShow~}
218
+ </div>
219
+ </div>
220
+ <!-- DefaultPackage end view template: [PRSP-AddFilter-Field] -->
221
+ `
222
+ },
223
+ {
224
+ Hash: 'PRSP-AddFilter-Clause',
225
+ Template: /*html*/`
226
+ <!-- DefaultPackage pict view template: [PRSP-AddFilter-Clause] -->
227
+ <button type="button" class="prsp-addfilter-clause" onclick="_Pict.views['PRSP-Filters'].addFilter(event, '{~D:AppData.PRSPAddFilter.RecordSet~}', '{~D:AppData.PRSPAddFilter.ViewContext~}', '{~D:Record.FilterKey~}', '{~D:Record.ClauseKey~}')">
228
+ <span class="prsp-addfilter-clause-ic">{~I:Plus~}</span>
229
+ <span>{~D:Record.DisplayName~}</span>
230
+ </button>
231
+ <!-- DefaultPackage end view template: [PRSP-AddFilter-Clause] -->
200
232
  `
201
233
  },
202
234
  ],
@@ -210,15 +242,15 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
210
242
  RenderMethod: 'replace'
211
243
  },
212
244
  {
213
- RenderableHash: 'PRSP-SUBSET-Filters-Template-AddFilter-Dropdown',
214
- TemplateHash: 'PRSP-SUBSET-Filters-Template-AddFilter-Dropdown',
215
- ContentDestinationAddress: '#PRSP-SUBSET-Filters-Template-AddFilter-Dropdown',
245
+ RenderableHash: 'PRSP_AddFilter_Popover',
246
+ TemplateHash: 'PRSP-AddFilter-Popover',
247
+ ContentDestinationAddress: '#PRSP_AddFilter_Popover',
216
248
  RenderMethod: 'replace',
217
249
  },
218
250
  {
219
- RenderableHash: 'PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-AddFilterClauseDropdown',
220
- TemplateHash: 'PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-AddFilterClauseDropdown',
221
- ContentDestinationAddress: '#PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-AddFilterClauseDropdown',
251
+ RenderableHash: 'PRSP_AddFilter_List',
252
+ TemplateHash: 'PRSP-AddFilter-List',
253
+ ContentDestinationAddress: '#PRSP_AddFilter_List',
222
254
  RenderMethod: 'replace',
223
255
  },
224
256
  ],
@@ -315,6 +347,15 @@ class ViewRecordSetSUBSETFilters extends libPictView
315
347
  // stale callback doesn't clobber DOM that now belongs to a different
316
348
  // filter experience.
317
349
  this._renderEpoch = 0;
350
+ // Add-filter popover state (module-owned, replaces the old native <select> pickers).
351
+ this._addFilterOpen = false;
352
+ this._addFilterRecordSet = null;
353
+ this._addFilterViewContext = null;
354
+ this._addFilterSearch = '';
355
+ this._addFilterExpandedKey = null;
356
+ // FilterKeys to hide from the add-filter popover (e.g. internal/audit columns). Host apps
357
+ // set this; it is merged with a per-record-set config `FilterFieldBlacklist`.
358
+ this.filterFieldBlacklist = [];
318
359
  }
319
360
 
320
361
  /**
@@ -617,12 +658,96 @@ class ViewRecordSetSUBSETFilters extends libPictView
617
658
  * @param {string} pRecordSet - The record set being filtered
618
659
  * @param {string} pViewContext - The view context for the filter (ex. List, Dashboard)
619
660
  */
620
- selectFilterToAdd(pEvent, pRecordSet, pViewContext)
661
+ toggleAddFilterPopover(pEvent, pRecordSet, pViewContext)
621
662
  {
622
- if (pEvent) pEvent.preventDefault();
623
- //const tmpRecordsetProvider = this.pict.providers['RSP-Provider-' + pRecordSet];
624
- //this.pict.log.info(`Selecting filter to add for record set: ${pRecordSet} in view context: ${pViewContext}`, tmpRecordsetProvider.getFilterSchema())
625
- this.renderWithScope(this.pict.providers[`RSP-Provider-${pRecordSet}`], 'PRSP-SUBSET-Filters-Template-AddFilter-Dropdown', undefined, { RecordSet: pRecordSet, ViewContext: pViewContext });
663
+ if (pEvent) { pEvent.preventDefault(); }
664
+ if (this._addFilterOpen)
665
+ {
666
+ return this.closeAddFilterPopover();
667
+ }
668
+ this._addFilterOpen = true;
669
+ this._addFilterRecordSet = pRecordSet;
670
+ this._addFilterViewContext = pViewContext;
671
+ this._addFilterSearch = '';
672
+ this._addFilterExpandedKey = null;
673
+ this._buildAddFilterFields(pRecordSet);
674
+ this.render('PRSP_AddFilter_Popover', undefined, { RecordSet: pRecordSet, ViewContext: pViewContext });
675
+ this._paintAddFilterOpenState();
676
+ }
677
+
678
+ /** Close the add-filter popover. */
679
+ closeAddFilterPopover()
680
+ {
681
+ this._addFilterOpen = false;
682
+ this._addFilterExpandedKey = null;
683
+ this._paintAddFilterOpenState();
684
+ }
685
+
686
+ /**
687
+ * Filter the add-filter field list by a search term, re-rendering only the list so the
688
+ * search input keeps focus.
689
+ * @param {string} pValue - The search term.
690
+ */
691
+ searchAddFilter(pValue)
692
+ {
693
+ this._addFilterSearch = pValue || '';
694
+ this._buildAddFilterFields(this._addFilterRecordSet);
695
+ this.render('PRSP_AddFilter_List', undefined, { RecordSet: this._addFilterRecordSet, ViewContext: this._addFilterViewContext });
696
+ }
697
+
698
+ /**
699
+ * Expand or collapse a field's available clauses in the add-filter popover.
700
+ * @param {string} pFilterKey - The field whose clauses to toggle.
701
+ */
702
+ toggleAddFilterField(pFilterKey)
703
+ {
704
+ this._addFilterExpandedKey = (this._addFilterExpandedKey === pFilterKey) ? null : pFilterKey;
705
+ this._buildAddFilterFields(this._addFilterRecordSet);
706
+ this.render('PRSP_AddFilter_List', undefined, { RecordSet: this._addFilterRecordSet, ViewContext: this._addFilterViewContext });
707
+ }
708
+
709
+ /**
710
+ * (Re)build the add-filter popover's field list into AppData from the record set's filter
711
+ * schema, honouring the current search term and the expanded field.
712
+ * @param {string} pRecordSet - The record set whose filter schema to read.
713
+ */
714
+ _buildAddFilterFields(pRecordSet)
715
+ {
716
+ const tmpProvider = this.pict.providers[`RSP-Provider-${pRecordSet}`];
717
+ const tmpSchema = (tmpProvider && typeof tmpProvider.getFilterSchema === 'function') ? tmpProvider.getFilterSchema() : {};
718
+ const tmpRecordSetConfig = this.pict.PictSectionRecordSet?.recordSetProviderConfigurations?.[pRecordSet] || {};
719
+ const tmpBlacklist = [].concat(this.filterFieldBlacklist || [], Array.isArray(tmpRecordSetConfig.FilterFieldBlacklist) ? tmpRecordSetConfig.FilterFieldBlacklist : []);
720
+ const tmpSearch = (this._addFilterSearch || '').toLowerCase();
721
+ const tmpFields = Object.values(tmpSchema)
722
+ .filter((pField) => Array.isArray(pField.AvailableClauses) && pField.AvailableClauses.length > 0)
723
+ .filter((pField) => !tmpBlacklist.includes(pField.FilterKey))
724
+ .filter((pField) => !tmpSearch || String(pField.DisplayName || pField.FilterKey).toLowerCase().includes(tmpSearch))
725
+ .sort((pA, pB) => String(pA.DisplayName || pA.FilterKey).localeCompare(String(pB.DisplayName || pB.FilterKey)))
726
+ .map((pField) =>
727
+ {
728
+ const tmpExpanded = (pField.FilterKey === this._addFilterExpandedKey);
729
+ return {
730
+ FilterKey: pField.FilterKey,
731
+ DisplayName: pField.DisplayName || pField.FilterKey,
732
+ ExpandedClass: tmpExpanded ? 'is-expanded' : '',
733
+ ClausesToShow: tmpExpanded ? pField.AvailableClauses : [],
734
+ };
735
+ });
736
+ this.pict.AppData.PRSPAddFilter =
737
+ {
738
+ RecordSet: pRecordSet,
739
+ ViewContext: this._addFilterViewContext,
740
+ Search: this._addFilterSearch || '',
741
+ IsEmpty: tmpFields.length === 0,
742
+ Fields: tmpFields,
743
+ };
744
+ }
745
+
746
+ /** Reflect the add-filter popover's open/closed state on its container element. */
747
+ _paintAddFilterOpenState()
748
+ {
749
+ const tmpPopover = document.getElementById('PRSP_AddFilter_Popover');
750
+ if (tmpPopover) { tmpPopover.classList.toggle('open', !!this._addFilterOpen); }
626
751
  }
627
752
 
628
753
  /**
@@ -681,11 +806,29 @@ class ViewRecordSetSUBSETFilters extends libPictView
681
806
  */
682
807
  onBeforeRender(pRenderable)
683
808
  {
809
+ // Re-assert ONLY the consolidated-control chrome (the wrapper, search row, filter toggle +
810
+ // Search button, the Filter Experiences container, and Clear/Reset/Apply) so the module owns
811
+ // that markup. Deliberately NOT the add-filter dropdown or per-clause templates: host apps
812
+ // (e.g. Headlight) layer their own styled add-filter popover and clause UI on top, and
813
+ // re-asserting those clobbers them — re-exposing bare native <select> dropdowns and a dead
814
+ // remove button. Those host overrides register at init and must survive each re-render.
815
+ const tmpChromeTemplateHashes =
816
+ [
817
+ 'PRSP-SUBSET-Filters-Template',
818
+ 'PRSP-SUBSET-Filters-Template-Input-Fieldset',
819
+ 'PRSP-SUBSET-Filters-Template-Button-Fieldset',
820
+ 'PRSP-SUBSET-Filters-Template-ManageFilters-Fieldset',
821
+ 'PRSP-SUBSET-Filters-Template-DrawerActions-Fieldset'
822
+ ];
684
823
  if (this.pict.TemplateProvider && Array.isArray(_DEFAULT_CONFIGURATION_SUBSET_Filter.Templates))
685
824
  {
686
- for (const tmpTemplate of _DEFAULT_CONFIGURATION_SUBSET_Filter.Templates)
825
+ for (const tmpTemplateHash of tmpChromeTemplateHashes)
687
826
  {
688
- this.pict.TemplateProvider.addTemplate(tmpTemplate.Hash, tmpTemplate.Template);
827
+ const tmpTemplate = _DEFAULT_CONFIGURATION_SUBSET_Filter.Templates.find((pTemplate) => pTemplate.Hash === tmpTemplateHash);
828
+ if (tmpTemplate)
829
+ {
830
+ this.pict.TemplateProvider.addTemplate(tmpTemplate.Hash, tmpTemplate.Template);
831
+ }
689
832
  }
690
833
  }
691
834
  return super.onBeforeRender(pRenderable);
@@ -698,34 +841,15 @@ class ViewRecordSetSUBSETFilters extends libPictView
698
841
  onAfterRender(pRenderable)
699
842
  {
700
843
  const res = super.onAfterRender(pRenderable);
701
- if (pRenderable?.RenderableHash === 'PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-AddFilterClauseDropdown')
844
+ // Add-filter popover sub-renders (the panel + its list) only repaint that widget — skip the
845
+ // heavy post-render pass below and just keep the open/closed class in sync.
846
+ if (pRenderable?.RenderableHash === 'PRSP_AddFilter_Popover' || pRenderable?.RenderableHash === 'PRSP_AddFilter_List')
702
847
  {
703
- return;
704
- }
705
- const tmpRecord = { };
706
- const tmpSelect = document.getElementById('PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-Select');
707
- if (tmpSelect)
708
- {
709
- const tmpActiveOption = document.getElementById('PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-Select')?.querySelector('option:checked');
710
- const tmpRecordSet = tmpActiveOption?.getAttribute('data-i-recordset');
711
- const tmpFilterKey = tmpActiveOption?.getAttribute('data-i-filter-key');
712
- const tmpViewContext = tmpSelect?.getAttribute('data-i-view-context');
713
- if (tmpRecordSet && tmpFilterKey)
714
- {
715
- const tmpProvider = this.pict.providers[`RSP-Provider-${tmpRecordSet}`];
716
- if (tmpProvider)
717
- {
718
- tmpRecord.RecordSet = tmpRecordSet;
719
- tmpRecord.FilterKey = tmpFilterKey;
720
- tmpRecord.ViewContext = tmpViewContext;
721
- tmpRecord.AvailableClauses = tmpProvider.getFilterClauseSchemaForKey(tmpFilterKey).AvailableClauses;
722
- if (Array.isArray(tmpRecord.AvailableClauses))
723
- {
724
- this.render('PRSP-SUBSET-Filters-Template-AddFilter-Dropdown-AddFilterClauseDropdown', undefined, tmpRecord, pRenderable);
725
- }
726
- }
727
- }
848
+ this._paintAddFilterOpenState();
849
+ return res;
728
850
  }
851
+ // Any full re-render rebuilds the add-filter slot, so the popover is closed — reset its state.
852
+ this._addFilterOpen = false;
729
853
  this.onMarshalToView();
730
854
 
731
855
  // NOTE: This is where we ensure the filter experience is applied after a render.
@@ -20,7 +20,49 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
20
20
  AutoSolveWithApp: false,
21
21
  AutoSolveOrdinal: 0,
22
22
 
23
- CSS: false,
23
+ // Themeable filter-clause chrome. Every filter type renders through the shared
24
+ // PRSP-Filter-Base-Template wrapper below (each type only plugs in its own value
25
+ // input via getFilterFormTemplate()), so styling it here themes every clause for
26
+ // every host app. This view (PRSP-FilterType-Base) is always constructed, and
27
+ // pict-view registers a view's CSS into the global cascade at construction, so
28
+ // these rules apply to all clauses regardless of which type rendered them.
29
+ //
30
+ // Host apps brand by defining the --theme-color-* tokens; the hardcoded values are
31
+ // sensible fallbacks. CSS-first: native <select>s are de-nativized with
32
+ // appearance:none + a custom chevron rather than swapped for a combobox widget
33
+ // (that is a later upgrade with cross-browser quirks of its own).
34
+ CSS: /*css*/`
35
+ .prsp-filters-clauses { display: flex; flex-direction: column; gap: 0.5rem; }
36
+
37
+ .prsp-filter { display: flex; align-items: flex-end; gap: 0.5rem; }
38
+ .prsp-filter *, .prsp-filter *::before, .prsp-filter *::after { box-sizing: border-box; }
39
+ .prsp-filter-body { flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.4rem 0.75rem; }
40
+
41
+ /* Inputs rendered by pict-section-form inside a clause. Match the consolidated
42
+ control's search box so clauses stop looking like bare native controls. */
43
+ .prsp-filter label { font: inherit; font-size: 0.78rem; font-weight: 600; display: block;
44
+ margin: 0 0 0.2rem; color: var(--theme-color-text-secondary, #45505f); }
45
+ .prsp-filter input, .prsp-filter select, .prsp-filter textarea { font: inherit; font-size: 0.92rem; width: 100%;
46
+ padding: 0.45rem 0.7rem; border-radius: 8px; border: 1px solid var(--theme-color-border-default, #d7dce3);
47
+ background: var(--theme-color-background-primary, #fff); color: var(--theme-color-text-primary, #1f2733); }
48
+ .prsp-filter input:focus, .prsp-filter select:focus, .prsp-filter textarea:focus { outline: none;
49
+ border-color: var(--theme-color-brand-primary, #156dd1);
50
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--theme-color-brand-primary, #156dd1) 16%, transparent); }
51
+ /* De-nativize the <select>: drop the platform arrow, add a themed chevron + room for it. */
52
+ .prsp-filter select { appearance: none; -webkit-appearance: none; -moz-appearance: none; cursor: pointer;
53
+ padding-right: 1.9rem;
54
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7686' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
55
+ background-repeat: no-repeat; background-position: right 0.6rem center; background-size: 1.05em; }
56
+
57
+ /* Remove (trash) control — one per clause, inherited by every filter type. */
58
+ .prsp-filter-remove { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
59
+ width: 2.05rem; height: 2.05rem; padding: 0; cursor: pointer; line-height: 1;
60
+ border-radius: 8px; border: 1px solid var(--theme-color-border-default, #d7dce3);
61
+ background: var(--theme-color-background-primary, #fff); color: var(--theme-color-text-muted, #6b7686); }
62
+ .prsp-filter-remove:hover { border-color: var(--theme-color-status-error, #d64545); color: var(--theme-color-status-error, #d64545);
63
+ background: color-mix(in srgb, var(--theme-color-status-error, #d64545) 12%, transparent); }
64
+ .prsp-filter-remove .pict-icon { font-size: 1.05rem; }
65
+ `,
24
66
  CSSPriority: 500,
25
67
 
26
68
  Manifests: {},
@@ -38,16 +80,21 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
38
80
  `
39
81
  },
40
82
  {
83
+ // The shared clause wrapper for every filter type. The type view plugs its own
84
+ // value input in via getFilterFormTemplate(); the themed remove control is
85
+ // inherited here (calls the base removeClause() method, reached through the
86
+ // always-registered PRSP-FilterType-<Type> view) so no host app re-implements one.
41
87
  Hash: 'PRSP-Filter-Base-Template',
42
88
  Template: /*html*/`
43
89
  <!-- DefaultPackage pict view template: [PRSP-Filter-Base-Template] -->
44
- <div id="PRSP_Filter_Container_{~D:Record.Hash~}">
45
- <button type="button"
46
- onclick="_Pict.views['PRSP-Filters'].removeFilter(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}', '{~D:Record.Hash~}');"
47
- >
48
- -
90
+ <div class="prsp-filter" id="PRSP_Filter_Container_{~D:Record.Hash~}" data-i-filter-hash="{~D:Record.Hash~}">
91
+ <div class="prsp-filter-body">
92
+ {~TBR:Context[0].getFilterFormTemplate()~}
93
+ </div>
94
+ <button type="button" class="prsp-filter-remove" title="Remove this filter" aria-label="Remove this filter"
95
+ onclick="_Pict.views['PRSP-FilterType-{~D:Record.Type~}'].removeClause(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}', '{~D:Record.Hash~}');">
96
+ {~I:Trash~}
49
97
  </button>
50
- {~TBR:Context[0].getFilterFormTemplate()~}
51
98
  </div>
52
99
  <!-- DefaultPackage end view template: [PRSP-Filter-Base-Template] -->
53
100
  `
@@ -99,6 +146,38 @@ class ViewRecordSetSUBSETFilterBase extends libPictView
99
146
  return 'PRSP-Filter-Base-Form';
100
147
  }
101
148
 
149
+ /**
150
+ * Remove this filter clause from its record set and re-render the filter control.
151
+ *
152
+ * Lives on the base view so every filter type inherits a single, themeable remove
153
+ * affordance and no host application has to re-implement one. Delegates to the
154
+ * consolidated control view (the canonical re-render path) and falls back to the
155
+ * record-set provider directly if that view is somehow unavailable.
156
+ *
157
+ * @param {Event} pEvent - The DOM event that triggered the removal.
158
+ * @param {string} pRecordSet - The record set being filtered.
159
+ * @param {string} pViewContext - The view context for the filter (e.g. List, Dashboard).
160
+ * @param {string} pHash - The hash of the specific filter clause to remove.
161
+ */
162
+ removeClause(pEvent, pRecordSet, pViewContext, pHash)
163
+ {
164
+ if (pEvent)
165
+ {
166
+ pEvent.preventDefault();
167
+ }
168
+ const tmpFiltersView = this.pict.views['PRSP-Filters'];
169
+ if (tmpFiltersView && typeof tmpFiltersView.removeFilter === 'function')
170
+ {
171
+ return tmpFiltersView.removeFilter(pEvent, pRecordSet, pViewContext, pHash);
172
+ }
173
+ // Fallback: the consolidated control view isn't present, so remove directly.
174
+ const tmpProvider = this.pict.providers[`RSP-Provider-${pRecordSet}`];
175
+ if (tmpProvider && typeof tmpProvider.removeFilterClause === 'function')
176
+ {
177
+ tmpProvider.removeFilterClause(pHash);
178
+ }
179
+ }
180
+
102
181
  /**
103
182
  * @return {string} - The prefix for the informary address.
104
183
  */
@@ -121,4 +200,3 @@ class ViewRecordSetSUBSETFilterBase extends libPictView
121
200
  module.exports = ViewRecordSetSUBSETFilterBase;
122
201
 
123
202
  module.exports.default_configuration = _DEFAULT_CONFIGURATION_SUBSET_Filter;
124
-