pict-section-recordset 1.3.1 → 1.5.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 (21) hide show
  1. package/package.json +1 -1
  2. package/source/views/Filter-PersistenceView.js +4 -0
  3. package/source/views/RecordSet-Filters.js +44 -3
  4. package/source/views/filters/RecordSet-Filter-EntityReference-Base.js +304 -0
  5. package/source/views/filters/RecordSet-Filter-ExternalJoinSelectedValue.js +10 -256
  6. package/source/views/filters/RecordSet-Filter-ExternalJoinSelectedValueList.js +10 -263
  7. package/source/views/filters/RecordSet-Filter-InternalJoinSelectedValue.js +10 -256
  8. package/source/views/filters/RecordSet-Filter-InternalJoinSelectedValueList.js +10 -263
  9. package/types/views/Filter-PersistenceView.d.ts.map +1 -1
  10. package/types/views/RecordSet-Filters.d.ts +8 -0
  11. package/types/views/RecordSet-Filters.d.ts.map +1 -1
  12. package/types/views/filters/RecordSet-Filter-EntityReference-Base.d.ts +37 -0
  13. package/types/views/filters/RecordSet-Filter-EntityReference-Base.d.ts.map +1 -0
  14. package/types/views/filters/RecordSet-Filter-ExternalJoinSelectedValue.d.ts +13 -18
  15. package/types/views/filters/RecordSet-Filter-ExternalJoinSelectedValue.d.ts.map +1 -1
  16. package/types/views/filters/RecordSet-Filter-ExternalJoinSelectedValueList.d.ts +13 -18
  17. package/types/views/filters/RecordSet-Filter-ExternalJoinSelectedValueList.d.ts.map +1 -1
  18. package/types/views/filters/RecordSet-Filter-InternalJoinSelectedValue.d.ts +13 -18
  19. package/types/views/filters/RecordSet-Filter-InternalJoinSelectedValue.d.ts.map +1 -1
  20. package/types/views/filters/RecordSet-Filter-InternalJoinSelectedValueList.d.ts +13 -18
  21. package/types/views/filters/RecordSet-Filter-InternalJoinSelectedValueList.d.ts.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict-section-recordset",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "Pict dynamic record set management views",
5
5
  "main": "source/Pict-Section-RecordSet.js",
6
6
  "files": [
@@ -29,6 +29,10 @@ const _DEFAULT_CONFIGURATION_FilterPersistenceView = (
29
29
  .prsp-exp-select, .prsp-exp-name { font: inherit; font-size: 0.9rem; padding: 0.42rem 0.6rem; border-radius: 8px;
30
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
31
  .prsp-exp-select { flex: 0 0 auto; width: 230px; min-width: 0; max-width: 100%; }
32
+ /* When a host layers select2 (or a similar widget) on the stored-filter <select>, its container
33
+ defaults to display:block at the row's full width, pushing Save/Delete onto a second line. Pin it to
34
+ a fixed-width inline flex item so the controls stay on one row. */
35
+ .prsp-exp-row .select2-container { flex: 0 0 auto !important; display: inline-block !important; width: 230px !important; max-width: 100%; vertical-align: middle; }
32
36
  .prsp-exp-select:focus, .prsp-exp-name:focus { outline: none; border-color: var(--theme-color-brand-primary, #156dd1);
33
37
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--theme-color-brand-primary, #156dd1) 16%, transparent); }
34
38
  .prsp-exp-btn { flex: 0 0 auto; white-space: nowrap; }
@@ -67,7 +67,9 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
67
67
  .prsp-filters-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 0.5rem; }
68
68
 
69
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: 280px; max-width: 360px; display: none; }
70
+ /* Fixed (viewport-anchored) + JS-positioned on open, so no ancestor overflow:hidden the filter card,
71
+ the slide-out drawer — can clip it, whatever the host's layout. */
72
+ .prsp-addfilter-pop { position: fixed; z-index: 30; min-width: 280px; max-width: 360px; display: none; }
71
73
  .prsp-addfilter-pop.open { display: block; }
72
74
  /* Transparent full-viewport backdrop: catches outside clicks to close (no document listener). */
73
75
  .prsp-addfilter-backdrop { position: fixed; inset: 0; z-index: 0; }
@@ -112,7 +114,7 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
112
114
  </form>
113
115
  <div class="prsp-filters-drawer" id="PRSP_Filter_Drawer">
114
116
  <div class="prsp-filters-drawer-inner">
115
- <div id="PRSP_Filter_Instances" class="prsp-filters-clauses">
117
+ <div id="PRSP_Filter_Instances" class="prsp-filters-clauses" onkeydown="if (event.key === 'Enter' &amp;&amp; !event.target.closest('.pps')) { event.preventDefault(); _Pict.views['PRSP-Filters'].handleSearch(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}'); }">
116
118
  {~FIV:Record~}
117
119
  </div>
118
120
  {~T:PRSP-SUBSET-Filters-Template-AddFilter-Fieldset~}
@@ -751,7 +753,46 @@ class ViewRecordSetSUBSETFilters extends libPictView
751
753
  _paintAddFilterOpenState()
752
754
  {
753
755
  const tmpPopover = document.getElementById('PRSP_AddFilter_Popover');
754
- if (tmpPopover) { tmpPopover.classList.toggle('open', !!this._addFilterOpen); }
756
+ if (!tmpPopover) { return; }
757
+ tmpPopover.classList.toggle('open', !!this._addFilterOpen);
758
+ if (this._addFilterOpen) { this._positionAddFilterPopover(tmpPopover); }
759
+ }
760
+
761
+ /**
762
+ * Position the (fixed) add-filter popover against its trigger button, flipping above when there's
763
+ * more room there. Fixed positioning means no ancestor overflow:hidden (the host's filter card, the
764
+ * slide-out drawer) can clip it — the price is we set its top/left from the trigger's rect here.
765
+ *
766
+ * @param {HTMLElement} pPopover - the #PRSP_AddFilter_Popover element (already display:block).
767
+ */
768
+ _positionAddFilterPopover(pPopover)
769
+ {
770
+ const tmpTrigger = document.getElementById('PRSP_Filter_Button_Add');
771
+ if (!tmpTrigger) { return; }
772
+ const tmpPanel = /** @type {HTMLElement} */ (pPopover.querySelector('.prsp-addfilter-panel'));
773
+ const tmpRect = tmpTrigger.getBoundingClientRect();
774
+ const tmpGap = 6;
775
+ const tmpMargin = 8;
776
+ const tmpVH = window.innerHeight;
777
+ const tmpVW = window.innerWidth;
778
+ const tmpWidth = pPopover.offsetWidth || 300;
779
+ pPopover.style.left = `${Math.round(Math.max(tmpMargin, Math.min(tmpRect.left, tmpVW - tmpWidth - tmpMargin)))}px`;
780
+ pPopover.style.right = 'auto';
781
+ const tmpSpaceBelow = tmpVH - tmpRect.bottom - tmpGap - tmpMargin;
782
+ const tmpSpaceAbove = tmpRect.top - tmpGap - tmpMargin;
783
+ // Prefer the natural downward direction; only flip above when the room below is genuinely cramped.
784
+ if (tmpSpaceBelow >= 220 || tmpSpaceBelow >= tmpSpaceAbove)
785
+ {
786
+ pPopover.style.top = `${Math.round(tmpRect.bottom + tmpGap)}px`;
787
+ pPopover.style.bottom = 'auto';
788
+ if (tmpPanel) { tmpPanel.style.maxHeight = `${Math.max(160, Math.min(tmpSpaceBelow, 460))}px`; }
789
+ }
790
+ else
791
+ {
792
+ pPopover.style.top = 'auto';
793
+ pPopover.style.bottom = `${Math.round(tmpVH - tmpRect.top + tmpGap)}px`;
794
+ if (tmpPanel) { tmpPanel.style.maxHeight = `${Math.max(160, Math.min(tmpSpaceAbove, 460))}px`; }
795
+ }
755
796
  }
756
797
 
757
798
  /**
@@ -0,0 +1,304 @@
1
+ const ViewRecordSetSUBSETFilterBase = require('./RecordSet-Filter-Base');
2
+
3
+ /**
4
+ * Shared base for the four entity-reference filter types — Internal/External join × single/list.
5
+ *
6
+ * These were ~95% duplicate views, each with the same bespoke "search results | selection" table UI
7
+ * and identical search/add/remove logic. This base consolidates all of it. Subclasses supply only the
8
+ * two seams that actually differ:
9
+ * - `getSearchEntity(pClause)` — internal join searches `RemoteTable`, external `ExternalFilterByTable`.
10
+ * - `isMultiSelect()` — the `…List` variants append; the plain `…SelectedValue` variants replace.
11
+ *
12
+ * Rendering uses the same mechanism as before (the input template is emitted during the clause's
13
+ * template parse, via `getFilterFormTemplate()`), so the consolidation is behavior-preserving. The
14
+ * input template is itself a seam (`getEntityInputTemplate()`) so a host can swap the table UI for a
15
+ * picker-backed input (a pict-section-form `Picker` InputType) without the module changing.
16
+ *
17
+ * Host-injected contextual scoping rides through `getContextScopeFilter(pClause)` — extra FoxHound
18
+ * stanzas AND-applied to the entity search (project / spec-year / …). The module never learns what
19
+ * those contexts mean; the host overrides the hook to read its own app state.
20
+ */
21
+ const _DEFAULT_CONFIGURATION_EntityReference =
22
+ {
23
+ ViewIdentifier: 'PRSP-FilterType-EntityReference-Base',
24
+
25
+ // When set (by a host), entity clauses delegate their input to this pict-section-form InputType
26
+ // (rendered via {~IWVDA:PSRSFilterProxyView:Record.ClauseDescriptor~}) instead of the built-in
27
+ // table UI — e.g. 'Picker' to use pict-section-picker. Default false → the table UI.
28
+ EntityInputType: false,
29
+
30
+ Templates:
31
+ [
32
+ {
33
+ // Table UI (default). Two cells: live search results | current selection.
34
+ Hash: 'PRSP-Filter-EntityRef-Table',
35
+ Template: /*html*/`
36
+ <table><tbody><tr>
37
+ <td valign="top">{~T:PRSP-Filter-EntityRef-SearchResults~}</td>
38
+ <td valign="top">{~T:PRSP-Filter-EntityRef-SelectedValues~}</td>
39
+ </tr></tbody></table>
40
+ `
41
+ },
42
+ {
43
+ // Picker / form-input delegation (opt-in via EntityInputType). Renders one dynamic input
44
+ // on the shared PSRSFilterProxyView from the clause's ClauseDescriptor (whose PictForm
45
+ // InputType is stamped in prepareRecord). Bound to the clause's Values address.
46
+ Hash: 'PRSP-Filter-EntityRef-Input',
47
+ Template: /*html*/`
48
+ {~IWVDA:PSRSFilterProxyView:Record.ClauseDescriptor~}
49
+ `
50
+ },
51
+ {
52
+ Hash: 'PRSP-Filter-EntityRef-SearchResults',
53
+ Template: /*html*/`
54
+ <form id="PRSP_Filter_{~D:Record.Hash~}_Search_Form" onsubmit="_Pict.views['{~D:Context[0].Hash~}'].performSearch(event, '{~D:Record.ClauseAddress~}', '{~D:Record.Hash~}'); return false;">
55
+ <input id="PRSP_Filter_{~D:Record.Hash~}_Search_Value" type="text" placeholder="Search..." value="{~D:Record.SearchInputValue~}" />
56
+ <button type="submit" id="PRSP_Filter_{~D:Record.Hash~}_Button_Search" onclick="_Pict.views['{~D:Context[0].Hash~}'].performSearch(event, '{~D:Record.ClauseAddress~}', '{~D:Record.Hash~}')">Search</button>
57
+ </form>
58
+ <table>
59
+ <thead><tr><th colspan="2">{~D:Record.Label~}</th></tr></thead>
60
+ <tbody>{~TSWP:PRSP-Filter-EntityRef-SearchResults-Entry:Record.SearchResults:Record~}</tbody>
61
+ </table>
62
+ <button type="button" id="PRSP_Filter_{~D:Record.Hash~}_Button_LoadMore" class="is-hidden" onclick="_Pict.views['{~D:Context[0].Hash~}'].loadMore(event, '{~D:Record.ClauseAddress~}', '{~D:Record.Hash~}', {~D:Record.SearchResultsOffset:0~})">Load More</button>
63
+ `
64
+ },
65
+ {
66
+ Hash: 'PRSP-Filter-EntityRef-SelectedValues',
67
+ Template: /*html*/`
68
+ <table>
69
+ <thead><tr><th colspan="2">Selection</th></tr></thead>
70
+ <tbody>{~TSWP:PRSP-Filter-EntityRef-SelectedValues-Entry:Record.SelectedValues:Record~}</tbody>
71
+ </table>
72
+ `
73
+ },
74
+ {
75
+ Hash: 'PRSP-Filter-EntityRef-SearchResults-Entry',
76
+ Template: /*html*/`
77
+ <tr><td><button onclick="_Pict.views['{~D:Context[0].Hash~}'].handleAdd(event, {~DVBK:Record.Data:Record.Payload.ExternalFilterTableLookupColumn~}, '{~D:Record.Payload.ClauseAddress~}', '{~D:Record.Payload.Hash~}')">Select</button></td><td>{~TFA:Record.Payload.ExternalRecordDisplayTemplate:Record~}</td></tr>
78
+ `
79
+ },
80
+ {
81
+ Hash: 'PRSP-Filter-EntityRef-SelectedValues-Entry',
82
+ Template: /*html*/`
83
+ <tr><td><button onclick="_Pict.views['{~D:Context[0].Hash~}'].handleRemove(event, {~DVBK:Record.Data:Record.Payload.ExternalFilterTableLookupColumn~}, '{~D:Record.Payload.ClauseAddress~}', '{~D:Record.Payload.Hash~}')">Remove</button></td><td>{~TFA:Record.Payload.ExternalRecordDisplayTemplate:Record~}</td></tr>
84
+ `
85
+ },
86
+ ],
87
+ };
88
+
89
+ class ViewRecordSetSUBSETFilterEntityReferenceBase extends ViewRecordSetSUBSETFilterBase
90
+ {
91
+ constructor(pFable, pOptions, pServiceHash)
92
+ {
93
+ if (!pOptions.PageSize)
94
+ {
95
+ pOptions.PageSize = 15;
96
+ }
97
+ super(pFable, pOptions, pServiceHash);
98
+ }
99
+
100
+ // --- Seams the subclasses override (the only real differences between the 4 types) ---
101
+
102
+ /** @param {Record<string, any>} pClause @return {string} The entity to search (RemoteTable / ExternalFilterByTable). */
103
+ getSearchEntity(pClause)
104
+ {
105
+ return pClause.RemoteTable || pClause.ExternalFilterByTable;
106
+ }
107
+
108
+ /** @param {Record<string, any>} pClause @return {string} The value/ID column on the searched entity. */
109
+ getLookupColumn(pClause)
110
+ {
111
+ return pClause.ExternalFilterTableLookupColumn || `ID${this.getSearchEntity(pClause)}`;
112
+ }
113
+
114
+ /** @return {boolean} True for the `…List` (multi-select) variants. */
115
+ isMultiSelect()
116
+ {
117
+ return true;
118
+ }
119
+
120
+ /**
121
+ * Host-injectable contextual search scope. Returns extra FoxHound filter stanza(s) AND-applied to
122
+ * the entity search (e.g. "only this project's line items"). Default: the clause's static
123
+ * `ContextScopeFilter` if any, else none. Hosts override this to read app state — the module never
124
+ * learns what a "project" or "spec year" is.
125
+ *
126
+ * @param {Record<string, any>} pClause @return {string|Array<string>}
127
+ */
128
+ getContextScopeFilter(pClause)
129
+ {
130
+ return pClause.ContextScopeFilter || '';
131
+ }
132
+
133
+ /** @return {string} The template hash for the clause's value input (table UI, or the opt-in delegated input). */
134
+ getFilterFormTemplate()
135
+ {
136
+ return this.options.EntityInputType ? 'PRSP-Filter-EntityRef-Input' : 'PRSP-Filter-EntityRef-Table';
137
+ }
138
+
139
+ /** @param {Record<string, any>} pRecord */
140
+ prepareRecord(pRecord)
141
+ {
142
+ super.prepareRecord(pRecord);
143
+
144
+ pRecord.ClauseDescriptor.PictForm = Object.assign({}, pRecord.PictForm);
145
+ if (this.options.EntityInputType)
146
+ {
147
+ // Delegated input: a csv string round-trips through the informary at `.StringArrayValue`,
148
+ // while the picker adapter writes the real ARRAY to `.Values` (what Filter.js reads) via
149
+ // ValueArrayAddress. Keeping the informary off `.Values` avoids a Number field defaulting it
150
+ // to "0". The descriptor also carries everything the picker needs + the contextual scope hook.
151
+ pRecord.ClauseDescriptor.Address = pRecord.ClauseAddress + '.StringArrayValue';
152
+ pRecord.ClauseDescriptor.DataType = 'String';
153
+ pRecord.ClauseDescriptor.PictForm.InputType = pRecord.ClauseDescriptor.PictForm.InputType || this.options.EntityInputType;
154
+ pRecord.ClauseDescriptor.PictForm.Entity = pRecord.ClauseDescriptor.PictForm.Entity || this.getSearchEntity(pRecord);
155
+ pRecord.ClauseDescriptor.PictForm.ValueField = pRecord.ClauseDescriptor.PictForm.ValueField || this.getLookupColumn(pRecord);
156
+ pRecord.ClauseDescriptor.PictForm.Multiple = this.isMultiSelect();
157
+ pRecord.ClauseDescriptor.PictForm.SearchFields = pRecord.ClauseDescriptor.PictForm.SearchFields
158
+ || pRecord.ExternalFilterByColumns || (pRecord.ExternalFilterByColumn ? [ pRecord.ExternalFilterByColumn ] : [ 'Name' ]);
159
+ pRecord.ClauseDescriptor.PictForm.ValueArrayAddress = pRecord.ClauseValuesAddress;
160
+ pRecord.ClauseDescriptor.PictForm.GetContextScopeFilter = () => this.getContextScopeFilter(this.getInformaryScopedValue(pRecord.ClauseAddress) || pRecord);
161
+ // JoinEntity compound display (host opt-in on the clause): show each searched row joined to a
162
+ // parent entity's field — e.g. a LineItem disambiguated by its Project. The picker fetch-then-
163
+ // merges the join (Meadow can't join in one read). Forwarded straight through; no-op when unset.
164
+ if (pRecord.JoinEntity || pRecord.ClauseDescriptor.PictForm.JoinEntity)
165
+ {
166
+ const tmpPF = pRecord.ClauseDescriptor.PictForm;
167
+ tmpPF.JoinEntity = tmpPF.JoinEntity || pRecord.JoinEntity;
168
+ tmpPF.JoinField = tmpPF.JoinField || pRecord.JoinField;
169
+ tmpPF.JoinEntityValueField = tmpPF.JoinEntityValueField || pRecord.JoinEntityValueField;
170
+ tmpPF.JoinEntityDisplayField = tmpPF.JoinEntityDisplayField || pRecord.JoinEntityDisplayField;
171
+ if (tmpPF.JoinEntityFirst === undefined && pRecord.JoinEntityFirst !== undefined) { tmpPF.JoinEntityFirst = pRecord.JoinEntityFirst; }
172
+ if (tmpPF.JoinSeparator === undefined && pRecord.JoinSeparator !== undefined) { tmpPF.JoinSeparator = pRecord.JoinSeparator; }
173
+ }
174
+ // Saved-filter seeding: mirror the live clause's Values array into the csv `.StringArrayValue`
175
+ // the input reads, so a reloaded/persisted filter shows its current selections on render.
176
+ const tmpLiveClause = this.getInformaryScopedValue(pRecord.ClauseAddress);
177
+ if (tmpLiveClause && Array.isArray(tmpLiveClause.Values))
178
+ {
179
+ tmpLiveClause.StringArrayValue = tmpLiveClause.Values.join(',');
180
+ }
181
+ }
182
+ else
183
+ {
184
+ pRecord.ClauseDescriptor.DataType = pRecord.DataType || 'Number';
185
+ }
186
+ if (!pRecord.ExternalFilterTableLookupColumn)
187
+ {
188
+ pRecord.ExternalFilterTableLookupColumn = this.getLookupColumn(pRecord);
189
+ }
190
+ if (!pRecord.SearchResults) { pRecord.SearchResults = []; }
191
+ if (!pRecord.SelectedValues) { pRecord.SelectedValues = []; }
192
+ }
193
+
194
+ // --- Table UI search/select methods (consolidated from the 4 legacy views) ---
195
+
196
+ loadMore(pEvent, pClauseInformaryAddress, pClauseHash, pCurrentOffset = 0)
197
+ {
198
+ this.performSearch(pEvent, pClauseInformaryAddress, pClauseHash, pCurrentOffset + this.options.PageSize);
199
+ }
200
+
201
+ performSearch(pEvent, pClauseInformaryAddress, pClauseHash, pOffset = 0)
202
+ {
203
+ if (pEvent) { pEvent.preventDefault(); }
204
+ const tmpClause = this.getInformaryScopedValue(pClauseInformaryAddress);
205
+ if (!tmpClause)
206
+ {
207
+ this.pict.log.error(`[Filter-EntityReference] No clause found for address: ${pClauseInformaryAddress}`);
208
+ return;
209
+ }
210
+ tmpClause.SearchResultsOffset = pOffset;
211
+ const tmpSearchInputValue = pOffset > 0 ? tmpClause.SearchInputValue : this.pict.ContentAssignment.readContent(`#PRSP_Filter_${tmpClause.Hash}_Search_Value`);
212
+ tmpClause.SearchInputValue = tmpSearchInputValue;
213
+ if (!tmpSearchInputValue)
214
+ {
215
+ tmpClause.SearchResults = [];
216
+ tmpClause.LoadMoreEnabled = false;
217
+ this._reRenderClause(tmpClause, pClauseInformaryAddress, pClauseHash);
218
+ return;
219
+ }
220
+ const tmpFilterByColumns = tmpClause.ExternalFilterByColumns || (tmpClause.ExternalFilterByColumn ? [ tmpClause.ExternalFilterByColumn ] : [ 'Name' ]);
221
+ const tmpSearchStanza = tmpFilterByColumns.map((pColumn) => `FBVOR~${pColumn}~LK~${encodeURIComponent(`%${tmpSearchInputValue}%`)}`).join('~');
222
+ const tmpScope = this.getContextScopeFilter(tmpClause);
223
+ const tmpScopeStanza = Array.isArray(tmpScope) ? tmpScope.filter(Boolean).join('~') : tmpScope;
224
+ const tmpMeadowFilter = [ tmpScopeStanza, tmpSearchStanza ].filter(Boolean).join('~');
225
+ this.pict.EntityProvider.gatherDataFromServer(
226
+ [
227
+ {
228
+ Entity: this.getSearchEntity(tmpClause),
229
+ Filter: tmpMeadowFilter,
230
+ Destination: pOffset > 0 ? `${this.getInformaryAddressPrefix()}${pClauseInformaryAddress}.SearchResultsAppend` : `${this.getInformaryAddressPrefix()}${pClauseInformaryAddress}.SearchResults`,
231
+ RecordStartCursor: pOffset,
232
+ PageSize: this.options.PageSize,
233
+ }
234
+ ],
235
+ () =>
236
+ {
237
+ if (pOffset > 0 && tmpClause.SearchResultsAppend?.length > 0)
238
+ {
239
+ tmpClause.SearchResults = tmpClause.SearchResults.concat(tmpClause.SearchResultsAppend);
240
+ }
241
+ const tmpLoadedRecords = pOffset > 0 ? tmpClause.SearchResultsAppend : tmpClause.SearchResults;
242
+ delete tmpClause.SearchResultsAppend;
243
+ tmpClause.SearchResultsOffset = pOffset;
244
+ tmpClause.LoadMoreEnabled = tmpLoadedRecords && tmpLoadedRecords.length >= this.options.PageSize;
245
+ this._reRenderClause(tmpClause, pClauseInformaryAddress, pClauseHash);
246
+ if (tmpClause.LoadMoreEnabled)
247
+ {
248
+ this.pict.ContentAssignment.removeClass(`#PRSP_Filter_${tmpClause.Hash}_Button_LoadMore`, 'is-hidden');
249
+ }
250
+ });
251
+ }
252
+
253
+ handleAdd(pEvent, pRecordLookupValue, pClauseInformaryAddress, pClauseHash)
254
+ {
255
+ if (pEvent) { pEvent.preventDefault(); }
256
+ const tmpClause = this.getInformaryScopedValue(pClauseInformaryAddress);
257
+ if (!tmpClause) { return; }
258
+ const tmpRecordLookupColumn = this.getLookupColumn(tmpClause);
259
+ const tmpRecordToAdd = (tmpClause.SearchResults || []).find((pRow) => pRow[tmpRecordLookupColumn] == pRecordLookupValue);
260
+ if (!tmpRecordToAdd) { return; }
261
+ const tmpValue = tmpRecordToAdd[tmpRecordLookupColumn];
262
+ if (tmpValue == null) { return; }
263
+ if (!tmpClause.SelectedValues) { tmpClause.SelectedValues = []; }
264
+ if (!tmpClause.Values) { tmpClause.Values = []; }
265
+ if (this.isMultiSelect())
266
+ {
267
+ if (tmpClause.SelectedValues.some((pSV) => pSV[tmpRecordLookupColumn] == pRecordLookupValue)) { return; }
268
+ tmpClause.SelectedValues.push(tmpRecordToAdd);
269
+ if (!tmpClause.Values.some((pV) => pV == tmpValue)) { tmpClause.Values.push(tmpValue); }
270
+ }
271
+ else
272
+ {
273
+ tmpClause.SelectedValues = [ tmpRecordToAdd ];
274
+ tmpClause.Values = [ tmpValue ];
275
+ }
276
+ this._reRenderClause(tmpClause, pClauseInformaryAddress, pClauseHash);
277
+ }
278
+
279
+ handleRemove(pEvent, pRecordLookupValue, pClauseInformaryAddress, pClauseHash)
280
+ {
281
+ if (pEvent) { pEvent.preventDefault(); }
282
+ const tmpClause = this.getInformaryScopedValue(pClauseInformaryAddress);
283
+ if (!tmpClause) { return; }
284
+ const tmpRecordLookupColumn = this.getLookupColumn(tmpClause);
285
+ const tmpIndex = (tmpClause.SelectedValues || []).findIndex((pRow) => pRow[tmpRecordLookupColumn] == pRecordLookupValue);
286
+ if (tmpIndex < 0) { return; }
287
+ const tmpRemoved = tmpClause.SelectedValues.splice(tmpIndex, 1)[0];
288
+ const tmpValue = tmpRemoved[tmpRecordLookupColumn];
289
+ tmpClause.Values = (tmpClause.Values || []).filter((pV) => pV !== tmpValue);
290
+ this._reRenderClause(tmpClause, pClauseInformaryAddress, pClauseHash);
291
+ }
292
+
293
+ /** Re-render a clause's container (the table UI re-paints on every search / add / remove). */
294
+ _reRenderClause(pClause, pClauseInformaryAddress, pClauseHash)
295
+ {
296
+ const tmpRecord = Object.assign({ ClauseAddress: pClauseInformaryAddress }, pClause);
297
+ this.prepareRecord(tmpRecord);
298
+ this.render(null, `#PRSP_Filter_Container_${pClauseHash}`, tmpRecord);
299
+ }
300
+ }
301
+
302
+ module.exports = ViewRecordSetSUBSETFilterEntityReferenceBase;
303
+
304
+ module.exports.default_configuration = Object.assign({}, ViewRecordSetSUBSETFilterBase.default_configuration, _DEFAULT_CONFIGURATION_EntityReference);
@@ -1,271 +1,25 @@
1
+ const ViewRecordSetSUBSETFilterEntityReferenceBase = require('./RecordSet-Filter-EntityReference-Base');
1
2
 
2
- const ViewRecordSetSUBSETFilterBase = require('./RecordSet-Filter-Base');
3
-
4
- const _DEFAULT_CONFIGURATION_Filter_ExternalJoin_SelectedValue =
3
+ // External-join (through a junction table), single-select entity-reference filter. Shared base;
4
+ // entity seam = ExternalFilterByTable, single-select (replace, not append).
5
+ const _DEFAULT_CONFIGURATION =
5
6
  {
6
7
  ViewIdentifier: 'PRSP-FilterType-ExternalJoinSelectedValue',
7
-
8
- Templates:
9
- [
10
- {
11
- Hash: 'PRSP-Filter-ExternalJoin-SelectedValue-Template',
12
- Template: /*html*/`
13
- <!-- DefaultPackage pict view template: [PRSP-Filter-ExternalJoin-SelectedValue-Template] -->
14
- <table>
15
- <tbody>
16
- <td valign="top">{~T:PRSP-Filter-ExternalJoin-SelectedValue-SearchResults~}</td><td valign="top">{~T:PRSP-Filter-ExternalJoin-SelectedValue-SelectedValues~}</td>
17
- </tbody>
18
- </table>
19
- <!-- DefaultPackage end view template: [PRSP-Filter-ExternalJoin-SelectedValue-Template] -->
20
- `
21
- },
22
- {
23
- Hash: 'PRSP-Filter-ExternalJoin-SelectedValue-SearchResults',
24
- Template: /*html*/`
25
- <!-- DefaultPackage pict view template: [PRSP-Filter-ExternalJoin-SearchResults] -->
26
- <form id="PRSP_Filter_{~D:Record.Hash~}_Search_Form" onsubmit="_Pict.views['{~D:Context[0].Hash~}'].performSearch(event, '{~D:Record.ClauseAddress~}', '{~D:Record.Hash~}'); return false;">
27
- <input id="PRSP_Filter_{~D:Record.Hash~}_Search_Value" type="text" placeholder="Search..." value="{~D:Record.SearchInputValue~}" />
28
- <button type="submit" id="PRSP_Filter_{~D:Record.Hash~}_Button_Search" onclick="_Pict.views['{~D:Context[0].Hash~}'].performSearch(event, '{~D:Record.ClauseAddress~}', '{~D:Record.Hash~}')">Search</button>
29
- </form>
30
- <table>
31
- <thead>
32
- <tr><th colspan="2">{~D:Record.Label~}</th></tr>
33
- </thead>
34
- <tbody>
35
- {~TSWP:PRSP-Filter-ExternalJoin-SelectedValue-SearchResults-Entry:Record.SearchResults:Record~}
36
- </tbody>
37
- </table>
38
- <button type="button" id="PRSP_Filter_{~D:Record.Hash~}_Button_LoadMore" class="is-hidden" onclick="_Pict.views['{~D:Context[0].Hash~}'].loadMore(event, '{~D:Record.ClauseAddress~}', '{~D:Record.Hash~}', {~D:Record.SearchResultsOffset:0~})">Load More</button>
39
- <!-- DefaultPackage end view template: [PRSP-Filter-ExternalJoin-SearchResults] -->
40
- `
41
- },
42
- {
43
- Hash: 'PRSP-Filter-ExternalJoin-SelectedValue-SelectedValues',
44
- Template: /*html*/`
45
- <!-- DefaultPackage view template: [PRSP-Filter-ExternalJoin-SelectedValue-SelectedValues] -->
46
- <table>
47
- <thead>
48
- <tr><th colspan="2">Selection</th></tr>
49
- </thead>
50
- <tbody>
51
- {~TSWP:PRSP-Filter-ExternalJoin-SelectedValue-SelectedValues-Entry:Record.SelectedValues:Record~}
52
- </tbody>
53
- </table>
54
- <!-- DefaultPackage end view template: [PRSP-Filter-ExternalJoin-SelectedValue-SelectedValues] -->
55
- `
56
- },
57
- {
58
- Hash: 'PRSP-Filter-ExternalJoin-SelectedValue-SearchResults-Entry',
59
- Template: /*html*/`
60
- <!-- DefaultPackage pict view template: [PRSP-Filter-ExternalJoin-SelectedValue-Template] -->
61
- <tr><td><button onclick="_Pict.views['{~D:Context[0].Hash~}'].handleSelect(event, {~DVBK:Record.Data:Record.Payload.ExternalFilterTableLookupColumn~}, '{~D:Record.Payload.ClauseAddress~}', '{~D:Record.Payload.Hash~}')">Select</button></td><td>{~TFA:Record.Payload.ExternalRecordDisplayTemplate:Record~}</td></tr>
62
- <!-- DefaultPackage end view template: [PRSP-Filter-ExternalJoin-SelectedValue-Template] -->
63
- `
64
- },
65
- {
66
- Hash: 'PRSP-Filter-ExternalJoin-SelectedValue-SelectedValues-Entry',
67
- Template: /*html*/`
68
- <!-- DefaultPackage pict view template: [PRSP-Filter-ExternalJoin-SelectedValue-SelectedValues-Entry] -->
69
- <tr><td><button onclick="_Pict.views['{~D:Context[0].Hash~}'].handleRemove(event, {~DVBK:Record.Data:Record.Payload.ExternalFilterTableLookupColumn~}, '{~D:Record.Payload.ClauseAddress~}', '{~D:Record.Payload.Hash~}')">Remove</button></td><td>{~TFA:Record.Payload.ExternalRecordDisplayTemplate:Record~}</td></tr>
70
- <!-- DefaultPackage end view template: [PRSP-Filter-ExternalJoin-SelectedValue-SelectedValues-Entry] -->
71
- `
72
- }
73
- ],
74
8
  };
75
9
 
76
- class ViewRecordSetSUBSETFilterExternalJoinSelectedValue extends ViewRecordSetSUBSETFilterBase
10
+ class ViewRecordSetSUBSETFilterExternalJoinSelectedValue extends ViewRecordSetSUBSETFilterEntityReferenceBase
77
11
  {
78
- constructor(pFable, pOptions, pServiceHash)
79
- {
80
- if (!pOptions.PageSize)
81
- {
82
- pOptions.PageSize = 15; // default page size for search results
83
- }
84
- super(pFable, pOptions, pServiceHash);
85
- /*
86
- * show / hide the complex editor since it'll be large ?
87
- * selected record lookup values
88
- * using bundle to load those records (if any)
89
- * templating out the selection
90
- * templating out the search results
91
- * controls to add search result values to selection
92
- */
93
- }
94
-
95
- /**
96
- * @param {Record<string, any>} pRecord
97
- */
98
- prepareRecord(pRecord)
99
- {
100
- super.prepareRecord(pRecord);
101
-
102
- pRecord.ClauseDescriptor.DataType = pRecord.DataType || 'Number';
103
- pRecord.ClauseDescriptor.PictForm = pRecord.PictForm || {};
104
- if (!pRecord.ExternalFilterTableLookupColumn)
105
- {
106
- pRecord.ExternalFilterTableLookupColumn = `ID${pRecord.ExternalFilterByTable}`;
107
- }
108
- if (!pRecord.SearchResults)
109
- {
110
- pRecord.SearchResults = [];
111
- }
112
- if (!pRecord.SelectedValues)
113
- {
114
- pRecord.SelectedValues = [];
115
- }
116
- }
117
-
118
- getFilterFormTemplate()
119
- {
120
- return 'PRSP-Filter-ExternalJoin-SelectedValue-Template';
121
- }
122
-
123
- /**
124
- * @param {UIEvent} pEvent
125
- * @param {string} pClauseInformaryAddress
126
- * @param {string} pClauseHash
127
- */
128
- loadMore(pEvent, pClauseInformaryAddress, pClauseHash, pCurrentOffset = 0)
129
- {
130
- this.performSearch(pEvent, pClauseInformaryAddress, pClauseHash, pCurrentOffset + this.options.PageSize);
131
- }
132
-
133
- /**
134
- * @param {UIEvent} pEvent
135
- * @param {string} pClauseInformaryAddress
136
- * @param {string} pClauseHash
137
- * @param {number} [pOffset=0] - The offset for the search results, defaults to 0
138
- */
139
- performSearch(pEvent, pClauseInformaryAddress, pClauseHash, pOffset = 0)
140
- {
141
- pEvent.preventDefault();
142
- const tmpClause = this.getInformaryScopedValue(pClauseInformaryAddress);
143
- if (!tmpClause)
144
- {
145
- this.pict.log.error(`[Filter-ExternalJoinSelectedValue] No clause found for address: ${pClauseInformaryAddress}`);
146
- return;
147
- }
148
- tmpClause.SearchResultsOffset = pOffset;
149
- // get the input value (search box)
150
- const tmpSearchInputValue = pOffset > 0 ? tmpClause.SearchInputValue : this.pict.ContentAssignment.readContent(`#PRSP_Filter_${tmpClause.Hash}_Search_Value`);
151
- tmpClause.SearchInputValue = tmpSearchInputValue;
152
- if (!tmpSearchInputValue)
153
- {
154
- tmpClause.SearchResults = [];
155
- tmpClause.LoadMoreEnabled = false;
156
- const tmpRecord = Object.assign({ ClauseAddress: pClauseInformaryAddress }, tmpClause);
157
- this.prepareRecord(tmpRecord);
158
- const tmpDestinationAddress = `#PRSP_Filter_Container_${pClauseHash}`;
159
- this.render(null, tmpDestinationAddress, tmpRecord);
160
- return;
161
- }
162
- const tmpFilterByColumns = tmpClause.ExternalFilterByColumns || (tmpClause.ExternalFilterByColumn ? [ tmpClause.ExternalFilterByColumn ] : [ 'Name' ]);
163
- const tmpMeadowFilter = tmpFilterByColumns.map((pColumn) => `FBVOR~${pColumn}~LK~${encodeURIComponent(`%${tmpSearchInputValue}%`)}`).join('~');
164
- // bundle load the remote records and put them in the clause
165
- this.pict.EntityProvider.gatherDataFromServer(
166
- [
167
- {
168
- Entity: tmpClause.ExternalFilterByTable,
169
- Filter: tmpMeadowFilter,
170
- Destination: pOffset > 0 ? `${this.getInformaryAddressPrefix()}${pClauseInformaryAddress}.SearchResultsAppend` : `${this.getInformaryAddressPrefix()}${pClauseInformaryAddress}.SearchResults`,
171
- RecordStartCursor: pOffset,
172
- PageSize: this.options.PageSize,
173
- }
174
- ],
175
- (pError, pResult) =>
176
- {
177
- if (pOffset > 0 && tmpClause.SearchResultsAppend?.length > 0)
178
- {
179
- tmpClause.SearchResults = tmpClause.SearchResults.concat(tmpClause.SearchResultsAppend);
180
- }
181
- delete tmpClause.SearchResultsAppend;
182
- tmpClause.SearchResultsOffset = pOffset;
183
- const tmpRecord = Object.assign({ ClauseAddress: pClauseInformaryAddress }, tmpClause);
184
- this.prepareRecord(tmpRecord);
185
- const tmpDestinationAddress = `#PRSP_Filter_Container_${pClauseHash}`;
186
- this.render(null, tmpDestinationAddress, tmpRecord);
187
- const tmpLoadedRecords = pOffset > 0 ? tmpClause.SearchResultsAppend : tmpClause.SearchResults;
188
- const tmpLoadMoreEnabled = tmpLoadedRecords && tmpLoadedRecords.length >= this.options.PageSize;
189
- tmpClause.LoadMoreEnabled = tmpLoadMoreEnabled;
190
- if (tmpClause.LoadMoreEnabled)
191
- {
192
- this.pict.ContentAssignment.removeClass(`#PRSP_Filter_${tmpClause.Hash}_Button_LoadMore`, 'is-hidden');
193
- }
194
- });
195
- }
196
-
197
- handleSelect(pEvent, pRecordLookupValue, pClauseInformaryAddress, pClauseHash)
12
+ getSearchEntity(pClause)
198
13
  {
199
- pEvent.preventDefault();
200
- const tmpClause = this.getInformaryScopedValue(pClauseInformaryAddress);
201
- if (!tmpClause)
202
- {
203
- this.pict.log.error(`[Filter-ExternalJoinSelectedValue] No clause found for address: ${pClauseInformaryAddress}`);
204
- return;
205
- }
206
- const tmpRecordLookupColumn = tmpClause.ExternalFilterTableLookupColumn || `ID${tmpClause.ExternalFilterByTable}`;
207
- const tmpRecordToSelect = tmpClause.SearchResults.find((r) => r[tmpRecordLookupColumn] == pRecordLookupValue);
208
- if (!tmpRecordToSelect)
209
- {
210
- this.pict.log.error(`[Filter-ExternalJoinSelectedValue] No record found to add for value: ${pRecordLookupValue}`);
211
- return;
212
- }
213
- if (!tmpClause.SelectedValues)
214
- {
215
- tmpClause.SelectedValues = [];
216
- }
217
- if (tmpClause.SelectedValues.some((pSV) => pSV[tmpRecordLookupColumn] == pRecordLookupValue))
218
- {
219
- return;
220
- }
221
- const tmpValue = tmpRecordToSelect[tmpRecordLookupColumn];
222
- if (tmpValue == null)
223
- {
224
- this.pict.log.error(`[Filter-ExternalJoinSelectedValue] No value found in record to add: ${JSON.stringify(tmpRecordToSelect)}`);
225
- return;
226
- }
227
- tmpClause.SelectedValues = [ tmpRecordToSelect ];
228
- tmpClause.Values = [ tmpValue ];
229
- const tmpRecord = Object.assign({ ClauseAddress: pClauseInformaryAddress }, tmpClause);
230
- this.prepareRecord(tmpRecord);
231
- const tmpDestinationAddress = `#PRSP_Filter_Container_${pClauseHash}`;
232
- this.render(null, tmpDestinationAddress, tmpRecord);
233
- if (tmpClause.LoadMoreEnabled)
234
- {
235
- this.pict.ContentAssignment.removeClass(`#PRSP_Filter_${tmpClause.Hash}_Button_LoadMore`, 'is-hidden');
236
- }
14
+ return pClause.ExternalFilterByTable;
237
15
  }
238
16
 
239
- handleRemove(pEvent, pRecordLookupValue, pClauseInformaryAddress, pClauseHash)
17
+ isMultiSelect()
240
18
  {
241
- pEvent.preventDefault();
242
- const tmpClause = this.getInformaryScopedValue(pClauseInformaryAddress);
243
- if (!tmpClause)
244
- {
245
- this.pict.log.error(`[Filter-ExternalJoinSelectedValue] No clause found for address: ${pClauseInformaryAddress}`);
246
- return;
247
- }
248
- const tmpRecordLookupColumn = tmpClause.ExternalFilterTableLookupColumn || `ID${tmpClause.ExternalFilterByTable}`;
249
- const tmpRecordIndexToRemove = tmpClause.SelectedValues.findIndex((r) => r[tmpRecordLookupColumn] == pRecordLookupValue);
250
- if (tmpRecordIndexToRemove < 0)
251
- {
252
- this.pict.log.error(`[Filter-ExternalJoinSelectedValue] No record found to remove for value: ${pRecordLookupValue}`);
253
- return;
254
- }
255
- const tmpRecordToRemove = tmpClause.SelectedValues.splice(tmpRecordIndexToRemove, 1)[0];
256
- const tmpValue = tmpRecordToRemove[tmpRecordLookupColumn];
257
- tmpClause.Values = tmpClause.Values.filter((pV) => pV !== tmpValue);
258
- const tmpRecord = Object.assign({ ClauseAddress: pClauseInformaryAddress }, tmpClause);
259
- this.prepareRecord(tmpRecord);
260
- const tmpDestinationAddress = `#PRSP_Filter_Container_${pClauseHash}`;
261
- this.render(null, tmpDestinationAddress, tmpRecord);
262
- if (tmpClause.LoadMoreEnabled)
263
- {
264
- this.pict.ContentAssignment.removeClass(`#PRSP_Filter_${tmpClause.Hash}_Button_LoadMore`, 'is-hidden');
265
- }
19
+ return false;
266
20
  }
267
21
  }
268
22
 
269
23
  module.exports = ViewRecordSetSUBSETFilterExternalJoinSelectedValue;
270
24
 
271
- module.exports.default_configuration = Object.assign({}, ViewRecordSetSUBSETFilterExternalJoinSelectedValue.default_configuration, _DEFAULT_CONFIGURATION_Filter_ExternalJoin_SelectedValue);
25
+ module.exports.default_configuration = Object.assign({}, ViewRecordSetSUBSETFilterEntityReferenceBase.default_configuration, _DEFAULT_CONFIGURATION);