pict-section-recordset 1.0.63 → 1.0.66

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.
@@ -0,0 +1,228 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ const _ViewConfiguration =
4
+ {
5
+ ViewIdentifier: "Bookstore-Navigation",
6
+
7
+ DefaultRenderable: "Bookstore-Navigation-Content",
8
+ DefaultDestinationAddress: "#Bookstore-Navigation-Container",
9
+
10
+ AutoRender: false,
11
+
12
+ CSS: /*css*/`
13
+ #Bookstore-Navigation-Container .pure-menu-heading
14
+ {
15
+ font-size: 110%;
16
+ color: #fff;
17
+ margin: 0;
18
+ background: #E76F51;
19
+ padding: 0.75em 0.6em;
20
+ }
21
+ .bookstore-nav-session
22
+ {
23
+ padding: 0.6em;
24
+ border-top: 1px solid #333;
25
+ font-size: 0.8rem;
26
+ color: #999;
27
+ }
28
+ .bookstore-nav-session .dot
29
+ {
30
+ display: inline-block;
31
+ width: 8px;
32
+ height: 8px;
33
+ border-radius: 50%;
34
+ background: #2A9D8F;
35
+ box-shadow: 0 0 4px rgba(42,157,143,0.5);
36
+ margin-right: 0.35em;
37
+ }
38
+ .bookstore-nav-session-name
39
+ {
40
+ display: block;
41
+ color: #D4A373;
42
+ font-weight: 600;
43
+ margin-top: 0.25em;
44
+ }
45
+ .bookstore-nav-logout
46
+ {
47
+ display: block;
48
+ margin-top: 0.5em;
49
+ background: #E76F51;
50
+ color: #fff;
51
+ border: none;
52
+ padding: 0.35em 0.75em;
53
+ border-radius: 4px;
54
+ font-size: 0.75rem;
55
+ font-weight: 600;
56
+ cursor: pointer;
57
+ width: 100%;
58
+ text-align: center;
59
+ }
60
+ .bookstore-nav-logout:hover
61
+ {
62
+ background: #C45A3E;
63
+ }
64
+ /* Preprocessor toggle section */
65
+ .bookstore-nav-preprocessor
66
+ {
67
+ padding: 0.6em;
68
+ border-top: 1px solid #333;
69
+ font-size: 0.75rem;
70
+ color: #999;
71
+ }
72
+ .bookstore-nav-preprocessor-label
73
+ {
74
+ display: block;
75
+ margin-bottom: 0.35em;
76
+ font-weight: 600;
77
+ color: #D4A373;
78
+ font-size: 0.7rem;
79
+ text-transform: uppercase;
80
+ letter-spacing: 0.05em;
81
+ }
82
+ .bookstore-preprocessor-toggle
83
+ {
84
+ position: relative;
85
+ display: inline-block;
86
+ width: 36px;
87
+ height: 20px;
88
+ vertical-align: middle;
89
+ }
90
+ .bookstore-preprocessor-toggle input
91
+ {
92
+ opacity: 0;
93
+ width: 0;
94
+ height: 0;
95
+ }
96
+ .bookstore-preprocessor-slider
97
+ {
98
+ position: absolute;
99
+ cursor: pointer;
100
+ top: 0;
101
+ left: 0;
102
+ right: 0;
103
+ bottom: 0;
104
+ background-color: #555;
105
+ transition: background-color 0.2s;
106
+ border-radius: 20px;
107
+ }
108
+ .bookstore-preprocessor-slider:before
109
+ {
110
+ position: absolute;
111
+ content: "";
112
+ height: 14px;
113
+ width: 14px;
114
+ left: 3px;
115
+ bottom: 3px;
116
+ background-color: #fff;
117
+ transition: transform 0.2s;
118
+ border-radius: 50%;
119
+ }
120
+ .bookstore-preprocessor-toggle input:checked + .bookstore-preprocessor-slider
121
+ {
122
+ background-color: #2A9D8F;
123
+ }
124
+ .bookstore-preprocessor-toggle input:checked + .bookstore-preprocessor-slider:before
125
+ {
126
+ transform: translateX(16px);
127
+ }
128
+ .bookstore-preprocessor-status
129
+ {
130
+ display: inline-block;
131
+ margin-left: 0.35em;
132
+ vertical-align: middle;
133
+ font-size: 0.7rem;
134
+ color: #999;
135
+ }
136
+ `,
137
+
138
+ Templates:
139
+ [
140
+ {
141
+ Hash: "Bookstore-Navigation-Template",
142
+ Template: /*html*/`
143
+ <span class="pure-menu-heading">Bookstore</span>
144
+ <ul class="pure-menu-list">
145
+ <li class="pure-menu-item"><a href="#" onclick="{~P~}.PictApplication.navigateTo('/Dashboard')" class="pure-menu-link">Dashboard</a></li>
146
+ <li class="pure-menu-item menu-item-divided"><a href="#" onclick="{~P~}.PictApplication.navigateTo('/Books')" class="pure-menu-link">Books</a></li>
147
+ <li class="pure-menu-item"><a href="#" onclick="{~P~}.PictApplication.navigateTo('/Authors')" class="pure-menu-link">Authors</a></li>
148
+ <li class="pure-menu-item"><a href="#" onclick="{~P~}.PictApplication.navigateTo('/BookStores')" class="pure-menu-link">Stores</a></li>
149
+ <li class="pure-menu-item menu-item-divided"><a href="#" onclick="{~P~}.PictApplication.navigateTo('/About')" class="pure-menu-link">About</a></li>
150
+ <li class="pure-menu-item"><a href="#" onclick="{~P~}.PictApplication.navigateTo('/Legal')" class="pure-menu-link">Legal</a></li>
151
+ </ul>
152
+ <div class="bookstore-nav-preprocessor">
153
+ <span class="bookstore-nav-preprocessor-label">Template Preprocessor</span>
154
+ <label class="bookstore-preprocessor-toggle">
155
+ <input type="checkbox" id="Bookstore-Preprocessor-Toggle" onclick="{~P~}.PictApplication.togglePreprocessor()">
156
+ <span class="bookstore-preprocessor-slider"></span>
157
+ </label>
158
+ <span class="bookstore-preprocessor-status" id="Bookstore-Preprocessor-Status">Off</span>
159
+ </div>
160
+ <div class="bookstore-nav-session">
161
+ <span class="dot"></span> Signed in
162
+ <span class="bookstore-nav-session-name" id="Bookstore-Nav-UserName"></span>
163
+ <button class="bookstore-nav-logout" type="button" onclick="{~P~}.PictApplication.doLogout()">Log out</button>
164
+ </div>
165
+ `
166
+ }
167
+ ],
168
+
169
+ Renderables:
170
+ [
171
+ {
172
+ RenderableHash: "Bookstore-Navigation-Content",
173
+ TemplateHash: "Bookstore-Navigation-Template",
174
+ DestinationAddress: "#Bookstore-Navigation-Container"
175
+ }
176
+ ]
177
+ };
178
+
179
+ class BookstoreNavigationView extends libPictView
180
+ {
181
+ constructor(pFable, pOptions, pServiceHash)
182
+ {
183
+ super(pFable, pOptions, pServiceHash);
184
+ }
185
+
186
+ onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
187
+ {
188
+ // Populate the user name from session data
189
+ let tmpSession = this.pict.AppData.Session;
190
+ let tmpDisplayName = '';
191
+
192
+ if (tmpSession && tmpSession.UserRecord)
193
+ {
194
+ tmpDisplayName = tmpSession.UserRecord.FullName
195
+ || tmpSession.UserRecord.LoginID
196
+ || '';
197
+ }
198
+
199
+ let tmpUserNameElements = this.services.ContentAssignment.getElement('#Bookstore-Nav-UserName');
200
+ if (tmpUserNameElements && tmpUserNameElements.length > 0)
201
+ {
202
+ tmpUserNameElements[0].textContent = tmpDisplayName;
203
+ }
204
+
205
+ // Sync the preprocessor toggle state with the application's current state
206
+ let tmpApp = this.pict.PictApplication;
207
+ let tmpEnabled = tmpApp && tmpApp.isPreprocessorEnabled && tmpApp.isPreprocessorEnabled();
208
+
209
+ let tmpToggleElements = this.services.ContentAssignment.getElement('#Bookstore-Preprocessor-Toggle');
210
+ if (tmpToggleElements && tmpToggleElements.length > 0)
211
+ {
212
+ tmpToggleElements[0].checked = !!tmpEnabled;
213
+ }
214
+
215
+ let tmpStatusElements = this.services.ContentAssignment.getElement('#Bookstore-Preprocessor-Status');
216
+ if (tmpStatusElements && tmpStatusElements.length > 0)
217
+ {
218
+ tmpStatusElements[0].textContent = tmpEnabled ? 'On' : 'Off';
219
+ tmpStatusElements[0].style.color = tmpEnabled ? '#2A9D8F' : '#999';
220
+ }
221
+
222
+ return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
223
+ }
224
+ }
225
+
226
+ module.exports = BookstoreNavigationView;
227
+
228
+ module.exports.default_configuration = _ViewConfiguration;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict-section-recordset",
3
- "version": "1.0.63",
3
+ "version": "1.0.66",
4
4
  "description": "Pict dynamic record set management views",
5
5
  "main": "source/Pict-Section-RecordSet.js",
6
6
  "directories": {
@@ -16,10 +16,10 @@
16
16
  "homepage": "https://github.com/stevenvelozo/pict-section-recordset#readme",
17
17
  "scripts": {
18
18
  "start": "node source/Pict-Section-RecordSet.js",
19
- "tests": "npx mocha -u tdd --exit -R spec --grep",
20
- "coverage": "npx nyc --reporter=lcov --reporter=text-lcov npx mocha -- -u tdd -R spec",
19
+ "tests": "npx quack test -g",
20
+ "coverage": "npx quack coverage",
21
21
  "build": "npx quack build",
22
- "test": "npx mocha -u tdd -R spec",
22
+ "test": "npx quack test",
23
23
  "lint": "eslint source",
24
24
  "types": "tsc -p tsconfig.build.json"
25
25
  },
@@ -33,19 +33,19 @@
33
33
  "browser-env": "^3.3.0",
34
34
  "eslint": "^9.28.0",
35
35
  "jquery": "^3.7.1",
36
- "pict": "^1.0.350",
37
- "pict-application": "^1.0.32",
38
- "pict-service-commandlineutility": "^1.0.18",
39
- "quackage": "^1.0.51",
36
+ "pict": "^1.0.363",
37
+ "pict-application": "^1.0.33",
38
+ "pict-service-commandlineutility": "^1.0.19",
39
+ "quackage": "^1.0.65",
40
40
  "typescript": "^5.9.3"
41
41
  },
42
42
  "dependencies": {
43
- "fable-serviceproviderbase": "^3.0.18",
44
- "pict-provider": "^1.0.10",
45
- "pict-router": "^1.0.4",
46
- "pict-section-form": "^1.0.189",
47
- "pict-template": "^1.0.14",
48
- "pict-view": "^1.0.66",
43
+ "fable-serviceproviderbase": "^3.0.19",
44
+ "pict-provider": "^1.0.12",
45
+ "pict-router": "^1.0.9",
46
+ "pict-section-form": "^1.0.196",
47
+ "pict-template": "^1.0.15",
48
+ "pict-view": "^1.0.68",
49
49
  "sinon": "^21.0.1"
50
50
  },
51
51
  "mocha": {
@@ -5,6 +5,48 @@ const libFilterViews = require('../views/filters/index.js');
5
5
  * Specialized instruction for rendering the filter view and plumbing in required context.
6
6
  *
7
7
  * Based on the Pict base {~V:...~} template instruction.
8
+ *
9
+ * ## Async render architecture (see renderAsync below)
10
+ *
11
+ * The async path is tuned to get the dashboard records list painted before any
12
+ * speculative filter-list REST lookups complete. Three things collaborate to
13
+ * make that happen:
14
+ *
15
+ * 1. **Parallel filter fan-out.** Every filter clause starts its own
16
+ * `renderWithScopeAsync` in a single pass, instead of chaining through a
17
+ * sequential `Anticipate` queue. Template parsing, `onBefore*`/`onProject*`
18
+ * microtask hops, and the synchronous DOM walks done inside each filter's
19
+ * `onAfterRender` all happen concurrently rather than serialized across N
20
+ * filters. Results are collected into an indexed array so final output
21
+ * order still matches the clause order.
22
+ *
23
+ * 2. **Per-filter transaction isolation.** Each filter render gets its own
24
+ * synthetic `RootRenderable` carrying a fresh `TransactionHash`. Any
25
+ * `virtual-assignment` sub-renders that the filter's dynamic form spawns
26
+ * (notably `PSRSFilterProxyView`, which pict-section-form uses to host
27
+ * each filter's input) push into THIS transaction's queue, not the
28
+ * dashboard's. The dashboard's outer `renderAsync` callback fires as soon
29
+ * as every filter has its template string - without waiting on any of
30
+ * the nested input-initialize work.
31
+ *
32
+ * 3. **Deferred post-render drain.** The filter's post-render pipeline
33
+ * (`onAfterRenderAsync`, which is what runs pict-section-form's
34
+ * `runInputProviderFunctions('onInputInitialize', ...)` and therefore
35
+ * triggers `EntityBundleRequest.gatherDataFromServer` for any
36
+ * speculative-load inputs) is intentionally NOT run inline. It is queued
37
+ * via `setTimeout(..., 0)` so it fires on the next macrotask, giving the
38
+ * browser a tick to paint the dashboard first.
39
+ *
40
+ * ### Render-epoch race guard
41
+ *
42
+ * Between the dashboard callback firing and the setTimeout actually running,
43
+ * the user may have navigated away / applied a different filter experience /
44
+ * cleared filters. The `PRSP-Filters` view owns a monotonic `_renderEpoch`
45
+ * counter that gets bumped on every mutating action (`performSearch`,
46
+ * `handleClear`, `handleReset`, `addFilter`, `removeFilter`). Each scheduled
47
+ * drain captures the epoch at schedule time and bails out if a newer render
48
+ * has invalidated it, so a stale REST response can never clobber a filter
49
+ * container that now belongs to a different experience.
8
50
  */
9
51
  class PictTemplateFilterInstanceViewInstruction extends libPictTemplate
10
52
  {
@@ -177,10 +219,61 @@ class PictTemplateFilterInstanceViewInstruction extends libPictTemplate
177
219
  {
178
220
  pRecord.ViewContext = pRecord.DashboardHash ? `${tmpViewContext}-${pRecord.DashboardHash}` : tmpViewContext;
179
221
  }
180
- const tmpAnticipate = this.pict.newAnticipate();
181
- let tmpResult = '';
182
222
 
183
223
  const tmpClauses = this.pict.Bundle._ActiveFilterState?.[pRecord.RecordSet]?.FilterClauses || [];
224
+ if (tmpClauses.length === 0)
225
+ {
226
+ return fCallback(null, '');
227
+ }
228
+
229
+ // Snapshot the current render epoch so the deferred drain below can
230
+ // detect a filter re-render that happened between schedule time and
231
+ // now (see class doc-comment, "Render-epoch race guard").
232
+ const tmpFiltersView = this.pict.views['PRSP-Filters'];
233
+ const tmpRenderEpoch = tmpFiltersView ? tmpFiltersView._renderEpoch : 0;
234
+
235
+ const tmpResults = new Array(tmpClauses.length).fill('');
236
+ const tmpDeferredDrains = [];
237
+ let tmpRemaining = tmpClauses.length;
238
+ let tmpCallbackFired = false;
239
+
240
+ const tmpFinalize = () =>
241
+ {
242
+ if (tmpCallbackFired || tmpRemaining > 0)
243
+ {
244
+ return;
245
+ }
246
+ tmpCallbackFired = true;
247
+ // Fire the dashboard callback first so the paint lands on the
248
+ // current tick, then defer the filters' onAfterRender work (which
249
+ // runs pict-section-form's input-initialize pass and fires any
250
+ // EntityBundleRequest REST calls) to the next macrotask.
251
+ fCallback(null, tmpResults.join(''));
252
+ if (tmpDeferredDrains.length === 0)
253
+ {
254
+ return;
255
+ }
256
+ setTimeout(() =>
257
+ {
258
+ // Epoch guard: bail out if a newer render has invalidated us.
259
+ if (tmpFiltersView && tmpFiltersView._renderEpoch !== tmpRenderEpoch)
260
+ {
261
+ return;
262
+ }
263
+ for (let d = 0; d < tmpDeferredDrains.length; d++)
264
+ {
265
+ try
266
+ {
267
+ tmpDeferredDrains[d]();
268
+ }
269
+ catch (pError)
270
+ {
271
+ this.log.warn(`Pict: Filter Instance Views Template Render: Error draining deferred filter transaction`, pError);
272
+ }
273
+ }
274
+ }, 0);
275
+ };
276
+
184
277
  //FIXME: lookup by hash instead?
185
278
  for (let i = 0; i < tmpClauses.length; i++)
186
279
  {
@@ -189,39 +282,63 @@ class PictTemplateFilterInstanceViewInstruction extends libPictTemplate
189
282
  const tmpView = this._getViewForFilterClause(tmpClause);
190
283
  if (!tmpView)
191
284
  {
285
+ tmpRemaining--;
192
286
  continue;
193
287
  }
194
288
 
195
289
  const tmpRenderGUID = this.pict.getUUID();
196
- tmpAnticipate.anticipate((fNext) =>
290
+ const tmpRecord = Object.assign({}, pRecord, tmpClause);
291
+ tmpRecord.ClauseAddress = `_ActiveFilterState[${pRecord.RecordSet}].FilterClauses[${i}]`;
292
+ tmpView.prepareRecord(tmpRecord);
293
+
294
+ // Synthetic per-filter root renderable: nested virtual-assignment
295
+ // renders from this filter's subtree push into the filter's own
296
+ // transaction queue instead of the dashboard's.
297
+ const tmpFilterTransactionHash = `FilterInstance-${tmpView.Hash}-${tmpRenderGUID}`;
298
+ this.pict.TransactionTracking.registerTransaction(tmpFilterTransactionHash);
299
+ const tmpFilterRootRenderable =
300
+ {
301
+ RenderableHash: '__Virtual',
302
+ TemplateHash: null,
303
+ ContentDestinationAddress: null,
304
+ RenderMethod: 'virtual-assignment',
305
+ TransactionHash: tmpFilterTransactionHash,
306
+ RootRenderableViewHash: tmpView.Hash,
307
+ };
308
+
309
+ tmpDeferredDrains.push(() =>
197
310
  {
198
- const tmpRecord = Object.assign({}, pRecord, tmpClause);
199
- tmpRecord.ClauseAddress = `_ActiveFilterState[${pRecord.RecordSet}].FilterClauses[${i}]`;
200
- tmpView.prepareRecord(tmpRecord);
311
+ tmpView.onAfterRenderAsync(() =>
312
+ {
313
+ // Remove the per-filter transaction we registered above
314
+ // once its drain completes. Pict-View will also attempt
315
+ // to unregister during its own onAfterRenderAsync chain;
316
+ // whichever call hits first wins, the other is a no-op.
317
+ this.pict.TransactionTracking.unregisterTransaction(tmpFilterTransactionHash);
318
+ }, tmpFilterRootRenderable);
319
+ });
201
320
 
202
- return tmpView.renderWithScopeAsync(tmpClause, '__Virtual', `__TemplateOutputCache.${tmpRenderGUID}`, tmpRecord, pState && pState.RootRenderable,
203
- (pError, pResult) =>
321
+ tmpView.renderWithScopeAsync(tmpClause, '__Virtual', `__TemplateOutputCache.${tmpRenderGUID}`, tmpRecord, tmpFilterRootRenderable,
322
+ (pError) =>
323
+ {
324
+ if (pError)
204
325
  {
205
- if (pError)
206
- {
207
- this.log.warn(`Pict: Filter Instance Views Template Render: Error rendering view [${tmpView.Hash}]`, pError);
208
- //TODO: should we fail the whole render?
209
- return fNext();
210
- //return fNext(pError);
211
- }
212
-
213
- tmpResult += this.pict.__TemplateOutputCache[tmpRenderGUID];
326
+ this.log.warn(`Pict: Filter Instance Views Template Render: Error rendering view [${tmpView.Hash}]`, pError);
327
+ //TODO: should we fail the whole render?
328
+ tmpResults[i] = '';
329
+ }
330
+ else
331
+ {
332
+ tmpResults[i] = this.pict.__TemplateOutputCache[tmpRenderGUID] || '';
214
333
  // TODO: Uncomment this when we like how it's working
215
334
  //delete this.pict.__TemplateOutputCache[tmpRenderGUID];
216
-
217
- return fNext(null);
218
- });
219
- });
335
+ }
336
+ tmpRemaining--;
337
+ tmpFinalize();
338
+ });
220
339
  }
221
- return tmpAnticipate.wait((pError) =>
222
- {
223
- return fCallback(pError, tmpResult);
224
- });
340
+ // Handle the case where every filter was skipped synchronously (no valid view).
341
+ tmpFinalize();
225
342
  }
226
343
  }
227
344
 
@@ -236,6 +236,26 @@ class ViewRecordSetSUBSETFilters extends libPictView
236
236
  this.newFilterSearchApplied = false;
237
237
  this.addFilterCallback = null;
238
238
  this.removeFilterCallback = null;
239
+ // Render-epoch counter, bumped any time the filter list is re-rendered
240
+ // or a new filter experience is applied. Deferred filter post-render
241
+ // work (e.g. the setTimeout-scheduled transaction drain in
242
+ // Pict-Template-FilterInstanceViews) captures the epoch at schedule
243
+ // time and compares against the current value before running, so a
244
+ // stale callback doesn't clobber DOM that now belongs to a different
245
+ // filter experience.
246
+ this._renderEpoch = 0;
247
+ }
248
+
249
+ /**
250
+ * Bump the render epoch. Call this whenever the active filter clauses are
251
+ * about to change in a way that would invalidate in-flight filter renders.
252
+ *
253
+ * @return {number} The new epoch value.
254
+ */
255
+ bumpRenderEpoch()
256
+ {
257
+ this._renderEpoch++;
258
+ return this._renderEpoch;
239
259
  }
240
260
 
241
261
  /**
@@ -350,6 +370,7 @@ class ViewRecordSetSUBSETFilters extends libPictView
350
370
  */
351
371
  performSearch(pRecordSet, pViewContext, pFilterString)
352
372
  {
373
+ this.bumpRenderEpoch();
353
374
  const tmpPictRouter = this.pict.providers.PictRouter;
354
375
  const tmpProviderConfiguration = this.pict.PictSectionRecordSet.recordSetProviderConfigurations[pRecordSet];
355
376
  let filterExpr = '';
@@ -410,6 +431,7 @@ class ViewRecordSetSUBSETFilters extends libPictView
410
431
  handleClear(pEvent, pRecordSet, pViewContext)
411
432
  {
412
433
  if (pEvent) pEvent.preventDefault();
434
+ this.bumpRenderEpoch();
413
435
  this.pict.ContentAssignment.assignContent('input[name="filter"]', '');
414
436
  this.pict.Bundle._ActiveFilterState[pRecordSet].FilterClauses = [];
415
437
  this.pict.providers.FilterDataProvider.removeDefaultFilterExperience(pRecordSet, pViewContext);
@@ -426,6 +448,7 @@ class ViewRecordSetSUBSETFilters extends libPictView
426
448
  handleReset(pEvent, pRecordSet, pViewContext)
427
449
  {
428
450
  if (pEvent) pEvent.preventDefault();
451
+ this.bumpRenderEpoch();
429
452
  this.pict.providers.FilterDataProvider.removeLastUsedFilterExperience(pRecordSet, pViewContext);
430
453
  this.pict.log.info(`Clearing filters for record set: ${pRecordSet} in view context: ${pViewContext}`);
431
454
  // Apply default filter experience if it exists, otherwise just clear
@@ -479,6 +502,7 @@ class ViewRecordSetSUBSETFilters extends libPictView
479
502
  addFilter(pEvent, pRecordSet, pViewContext, pFilterKey, pClauseKey)
480
503
  {
481
504
  if (pEvent) pEvent.preventDefault();
505
+ this.bumpRenderEpoch();
482
506
  this.pict.log.info(`Adding filter: ${pFilterKey} with clause: ${pClauseKey} to record set: ${pRecordSet} in view context: ${pViewContext}`);
483
507
  this.pict.providers[`RSP-Provider-${pRecordSet}`].addFilterClause(pFilterKey, pClauseKey);
484
508
  //FIXME: we need the record from the original render here but no longer have it...
@@ -495,6 +519,7 @@ class ViewRecordSetSUBSETFilters extends libPictView
495
519
  removeFilter(pEvent, pRecordSet, pViewContext, pSpecificFilterKey)
496
520
  {
497
521
  if (pEvent) pEvent.preventDefault();
522
+ this.bumpRenderEpoch();
498
523
  this.pict.log.info(`Removing filter: ${pSpecificFilterKey} from record set: ${pRecordSet} in view context: ${pViewContext}`);
499
524
  this.pict.providers[`RSP-Provider-${pRecordSet}`].removeFilterClause(pSpecificFilterKey);
500
525
  //FIXME: we need the record from the original render here but no longer have it...