pict-section-recordset 1.9.6 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/package.json +2 -2
- package/source/Pict-Section-RecordSet.js +1 -0
- package/source/providers/Column-Data-Provider.js +219 -0
- package/source/providers/RecordSet-RecordProvider-Base.js +64 -1
- package/source/providers/RecordSet-RecordProvider-MeadowEndpoints.js +92 -16
- package/source/services/RecordsSet-MetaController.js +23 -1
- package/source/templates/Pict-Template-FilterInstanceViews.js +4 -0
- package/source/views/RecordSet-Filters.js +140 -3
- package/source/views/filters/RecordSet-Filter-DistinctSelectedValueList.js +233 -0
- package/source/views/filters/index.js +2 -0
- package/source/views/list/RecordSet-List-ColumnChooser.js +345 -0
- package/source/views/list/RecordSet-List-RecordListEntry.js +4 -1
- package/source/views/list/RecordSet-List.js +390 -15
- package/source/views/read/RecordSet-Read.js +65 -6
- package/types/Pict-Section-RecordSet.d.ts +1 -0
- package/types/providers/Column-Data-Provider.d.ts +115 -0
- package/types/providers/Column-Data-Provider.d.ts.map +1 -0
- package/types/providers/RecordSet-DynamicRecordsetSolver.d.ts +3 -0
- package/types/providers/RecordSet-DynamicRecordsetSolver.d.ts.map +1 -1
- package/types/providers/RecordSet-RecordProvider-Base.d.ts +110 -0
- package/types/providers/RecordSet-RecordProvider-Base.d.ts.map +1 -1
- package/types/providers/RecordSet-RecordProvider-MeadowEndpoints.d.ts +51 -1
- package/types/providers/RecordSet-RecordProvider-MeadowEndpoints.d.ts.map +1 -1
- package/types/providers/RecordSet-Router.d.ts +1 -0
- package/types/providers/RecordSet-Router.d.ts.map +1 -1
- package/types/services/RecordsSet-MetaController.d.ts.map +1 -1
- package/types/templates/Pict-Template-FilterInstanceViews.d.ts.map +1 -1
- package/types/views/RecordSet-Filters.d.ts +61 -0
- package/types/views/RecordSet-Filters.d.ts.map +1 -1
- package/types/views/RecordSet-RecordBaseView.d.ts +1 -0
- package/types/views/RecordSet-RecordBaseView.d.ts.map +1 -1
- package/types/views/filters/RecordSet-Filter-EntityReference-Base.d.ts.map +1 -1
- package/types/views/list/RecordSet-List-ColumnChooser.d.ts +68 -0
- package/types/views/list/RecordSet-List-ColumnChooser.d.ts.map +1 -0
- package/types/views/list/RecordSet-List-RecordListEntry.d.ts.map +1 -1
- package/types/views/list/RecordSet-List.d.ts +167 -2
- package/types/views/list/RecordSet-List.d.ts.map +1 -1
- package/types/views/read/RecordSet-Read.d.ts +8 -0
- package/types/views/read/RecordSet-Read.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,44 @@ The control type is inferred from the field's clause (text for a string match, a
|
|
|
43
43
|
|
|
44
44
|
**Turning it off.** A single record set opts out with `QuickFilters: false`. To make quick filters **opt-in** across a whole app — only record sets with an explicit `QuickFilters` array show the bar — set the filter view's flag once: `pict.views['PRSP-Filters'].quickFiltersAutoDefault = false`.
|
|
45
45
|
|
|
46
|
+
## Column Chooser
|
|
47
|
+
|
|
48
|
+
An opt-in, per-record-set "Columns" button above the list table that lets the user show/hide columns. Three tiers of candidates:
|
|
49
|
+
|
|
50
|
+
- **Curated** — the columns the host declared (manifest `Descriptors` or `RecordSetListColumns`), in declared order. Default visible; a column or descriptor can ship hidden-by-default with `DefaultHidden: true` (proper display name + cell template, one click away).
|
|
51
|
+
- **Schema** — every remaining scalar column on the entity (identity/audit fields and blob `Text`/`JSON` columns excluded), listed under "More columns", default hidden, rendered with the generic `{~ProcessCell~}` template (entity-reference `ID*` columns resolve names automatically).
|
|
52
|
+
- **Audit** — the identity pair and audit stamps with friendly labels (ID, GUID, Created, Created by, Updated, Updated by, Deleted, Deleted on, Deleted by), listed under "Audit columns", default hidden. The user-reference stamps resolve to user names like any entity column.
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
{
|
|
56
|
+
"RecordSet": "Book",
|
|
57
|
+
"RecordSetListColumnChooser": true,
|
|
58
|
+
"RecordSetListColumns":
|
|
59
|
+
[
|
|
60
|
+
{ "Key": "Title" },
|
|
61
|
+
{ "Key": "Genre" },
|
|
62
|
+
{ "Key": "ISBN", "DefaultHidden": true }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Toggling a column repaints only the rows + pagination (the filter bar and its state are never re-rendered). Under `RecordSetListLiteFetch`, showing a schema-tier column whose data wasn't fetched triggers exactly one refetch with the projection widened to include it.
|
|
68
|
+
|
|
69
|
+
**Persistence.** Choices persist per browser in localStorage (`Column_Meta_{RecordSet}_List`), with a session mirror at `pict.Bundle._ActiveColumnState`. To persist somewhere else (e.g. a per-user server preference), register your own provider as `ColumnDataProvider` **before** `PictSectionRecordSet.initialize()` — the section only registers the built-in one when none exists. The class is exported as `PictSectionRecordSet.ColumnDataProvider` for subclassing.
|
|
70
|
+
|
|
71
|
+
**Host hook note.** `onBeforeRenderList` is (re-)invoked on every paint, including column-visibility repaints. `TableCells` is rebuilt from pristine candidates each paint so cell-level mutations apply exactly once — but record decoration done in the hook must be idempotent. Cells the hook appends are unmanaged by the chooser: they always render and never appear in the chooser list.
|
|
72
|
+
|
|
73
|
+
## Show Deleted
|
|
74
|
+
|
|
75
|
+
Meadow soft-deletes: lists normally return only `Deleted = 0` rows. Set `RecordSetListShowDeletedFilter: true` on a record set to add a **"Show deleted"** checkbox to the filter drawer's footer (next to Clear / Reset / Apply). The switch is a **real clause** — a `RawFilter` referencing the Deleted column (`FBL~Deleted~INN~0,1`), which takes over FoxHound's automatic delete tracking so active and deleted rows enumerate together. Because it's a clause:
|
|
76
|
+
|
|
77
|
+
- It **serializes into the route URL** (the FilterExperience segment) — reloads and shared links keep it.
|
|
78
|
+
- Toggling applies through the normal search flow (the URL always changes, so the fetch always fires).
|
|
79
|
+
- **Clear** drops it like any other clause; the drawer's clause list skips it (the checkbox is its face).
|
|
80
|
+
- Records and counts stay in sync (both flow through the same clause assembly).
|
|
81
|
+
|
|
82
|
+
Deleted rows render at reduced opacity (`prsp-row-deleted`), and their default **View** link routes to `/PSRS/:RecordSet/ViewDeleted/:GUID` — a read route whose lookup explicitly includes soft-deleted records (a plain View would find nothing) and which renders a "This record has been deleted" banner above the record. Pair with the audit tier's *Deleted / Deleted on / Deleted by* columns in the column chooser.
|
|
83
|
+
|
|
46
84
|
## Related Packages
|
|
47
85
|
|
|
48
86
|
- [pict](https://github.com/fable-retold/pict) - MVC application framework
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-section-recordset",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Pict dynamic record set management views",
|
|
5
5
|
"main": "source/Pict-Section-RecordSet.js",
|
|
6
6
|
"files": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"browser-env": "^3.3.0",
|
|
38
38
|
"eslint": "^9.28.0",
|
|
39
39
|
"jquery": "^3.7.1",
|
|
40
|
-
"pict": "^1.0.
|
|
40
|
+
"pict": "^1.0.384",
|
|
41
41
|
"pict-application": "^1.0.34",
|
|
42
42
|
"pict-docuserve": "^1.4.19",
|
|
43
43
|
"pict-service-commandlineutility": "^1.0.19",
|
|
@@ -10,3 +10,4 @@ module.exports.PictRecordSetApplication = require('./application/Pict-Applicatio
|
|
|
10
10
|
// Export the providers
|
|
11
11
|
module.exports.RecordSetProviderBase = require('./providers/RecordSet-RecordProvider-Base.js');
|
|
12
12
|
module.exports.RecordSetProviderMeadowEndpoints = require('./providers/RecordSet-RecordProvider-MeadowEndpoints.js');
|
|
13
|
+
module.exports.ColumnDataProvider = require('./providers/Column-Data-Provider.js');
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
const libPictProvider = require('pict-provider');
|
|
2
|
+
|
|
3
|
+
const _DEFAULT_PROVIDER_CONFIGURATION =
|
|
4
|
+
{
|
|
5
|
+
ProviderIdentifier: 'ColumnDataProvider',
|
|
6
|
+
AutoInitialize: true,
|
|
7
|
+
AutoInitializeOrdinal: 0,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Terminology for Column Data Provider (to avoid confusion):
|
|
11
|
+
* A "Record Set" is a collection of records rendered as a list with columns.
|
|
12
|
+
* A "Column Visibility Override" is a per-column user choice (true = show, false = hide)
|
|
13
|
+
* that overrides the column's default visibility (visible unless DefaultHidden).
|
|
14
|
+
* Columns with no override entry render at their default visibility.
|
|
15
|
+
|
|
16
|
+
* Behavior Summary:
|
|
17
|
+
* - Save the per-recordset override map to LocalStorage under a key derived from
|
|
18
|
+
* Record Set and View Context.
|
|
19
|
+
* - Mirror the override map into pict.Bundle._ActiveColumnState[RecordSet] so reads
|
|
20
|
+
* are synchronous and consistent within a session (the Meadow record provider reads
|
|
21
|
+
* this at fetch time to widen Lite projections before the list view composes columns).
|
|
22
|
+
* - Clear both on "Reset to defaults".
|
|
23
|
+
|
|
24
|
+
* Storage Key Structure:
|
|
25
|
+
* - Column_Meta_{RecordSet}_{ViewContext} : stores the Column Meta JSON.
|
|
26
|
+
|
|
27
|
+
* Object Shape for Column Meta:
|
|
28
|
+
* {
|
|
29
|
+
* RecordSet: string, (auto-filled on save)
|
|
30
|
+
* ViewContext: string, (auto-filled on save; 'List' for the list view)
|
|
31
|
+
* Overrides: { [ColumnKey: string]: boolean },
|
|
32
|
+
* LastModifiedDate: string (ISO date) (auto-filled on save)
|
|
33
|
+
* }
|
|
34
|
+
|
|
35
|
+
* Host override contract:
|
|
36
|
+
* - To persist column choices somewhere other than LocalStorage (e.g. a per-user
|
|
37
|
+
* server-side preference), register your own provider AS 'ColumnDataProvider'
|
|
38
|
+
* BEFORE PictSectionRecordSet.initialize() runs — the section only registers this
|
|
39
|
+
* one when no provider with that hash exists yet. Load remote prefs at app start
|
|
40
|
+
* and seed them through setColumnVisibilityOverride (or write the Bundle mirror
|
|
41
|
+
* directly); the read methods here must stay synchronous.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
class ColumnDataProvider extends libPictProvider
|
|
45
|
+
{
|
|
46
|
+
/**
|
|
47
|
+
* @param {import('pict')} pFable - The Fable instance
|
|
48
|
+
* @param {Record<string, any>} [pOptions] - The options for the provider
|
|
49
|
+
* @param {string} [pServiceHash] - The service hash for the provider
|
|
50
|
+
*/
|
|
51
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
52
|
+
{
|
|
53
|
+
let tmpOptions = Object.assign({}, _DEFAULT_PROVIDER_CONFIGURATION, pOptions);
|
|
54
|
+
super(pFable, tmpOptions, pServiceHash);
|
|
55
|
+
|
|
56
|
+
// This allows unit tests to run
|
|
57
|
+
this.storageProvider = this;
|
|
58
|
+
this.keyCache = {};
|
|
59
|
+
if ((typeof(window) === 'object') && (typeof(window.localStorage) === 'object'))
|
|
60
|
+
{
|
|
61
|
+
this.storageProvider = window.localStorage;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the LocalStorage key for a record set's column visibility overrides.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} pRecordSet - The record set the overrides belong to
|
|
69
|
+
* @param {string} [pViewContext] - The view context (defaults to 'List')
|
|
70
|
+
* @return {string} The storage key for the column meta record.
|
|
71
|
+
*/
|
|
72
|
+
getColumnStorageKey(pRecordSet, pViewContext)
|
|
73
|
+
{
|
|
74
|
+
return `Column_Meta_${pRecordSet}_${pViewContext || 'List'}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the column visibility override map for a record set.
|
|
79
|
+
*
|
|
80
|
+
* Bundle-first: the session mirror in pict.Bundle._ActiveColumnState wins; on a
|
|
81
|
+
* miss the stored meta is read and seeded into the Bundle so every later read
|
|
82
|
+
* (including the Meadow provider's fetch-time read) is synchronous and consistent.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} pRecordSet - The record set to get overrides for
|
|
85
|
+
* @param {string} [pViewContext] - The view context (defaults to 'List')
|
|
86
|
+
* @return {Record<string, boolean>} The override map (empty object when none).
|
|
87
|
+
*/
|
|
88
|
+
getColumnVisibilityOverrides(pRecordSet, pViewContext)
|
|
89
|
+
{
|
|
90
|
+
if (!pRecordSet)
|
|
91
|
+
{
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
const tmpActiveColumnState = this.pict.Bundle._ActiveColumnState;
|
|
95
|
+
if (tmpActiveColumnState && tmpActiveColumnState[pRecordSet] && tmpActiveColumnState[pRecordSet].Overrides)
|
|
96
|
+
{
|
|
97
|
+
return tmpActiveColumnState[pRecordSet].Overrides;
|
|
98
|
+
}
|
|
99
|
+
/** @type {Record<string, boolean>} */
|
|
100
|
+
let tmpOverrides = {};
|
|
101
|
+
const tmpColumnMetaJSON = this.storageProvider.getItem(this.getColumnStorageKey(pRecordSet, pViewContext));
|
|
102
|
+
if (tmpColumnMetaJSON)
|
|
103
|
+
{
|
|
104
|
+
try
|
|
105
|
+
{
|
|
106
|
+
const tmpColumnMeta = JSON.parse(tmpColumnMetaJSON);
|
|
107
|
+
if (tmpColumnMeta && typeof(tmpColumnMeta.Overrides) === 'object' && tmpColumnMeta.Overrides !== null)
|
|
108
|
+
{
|
|
109
|
+
tmpOverrides = tmpColumnMeta.Overrides;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (pError)
|
|
113
|
+
{
|
|
114
|
+
this.pict.log.warn(`ColumnDataProvider: could not parse stored column meta for [${pRecordSet}]: ${pError.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this._seedBundleColumnState(pRecordSet, tmpOverrides);
|
|
118
|
+
return tmpOverrides;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Set (and persist) a single column visibility override for a record set.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} pRecordSet - The record set the column belongs to
|
|
125
|
+
* @param {string} pViewContext - The view context ('List' for the list view; falsy defaults to 'List')
|
|
126
|
+
* @param {string} pKey - The column key
|
|
127
|
+
* @param {boolean} pVisible - Whether the column should be visible
|
|
128
|
+
* @return {Record<string, boolean>} The updated override map.
|
|
129
|
+
*/
|
|
130
|
+
setColumnVisibilityOverride(pRecordSet, pViewContext, pKey, pVisible)
|
|
131
|
+
{
|
|
132
|
+
const tmpOverrides = this.getColumnVisibilityOverrides(pRecordSet, pViewContext);
|
|
133
|
+
tmpOverrides[pKey] = (pVisible === true);
|
|
134
|
+
this._seedBundleColumnState(pRecordSet, tmpOverrides);
|
|
135
|
+
const tmpColumnMeta =
|
|
136
|
+
{
|
|
137
|
+
RecordSet: pRecordSet,
|
|
138
|
+
ViewContext: pViewContext || 'List',
|
|
139
|
+
Overrides: tmpOverrides,
|
|
140
|
+
LastModifiedDate: new Date().toISOString(),
|
|
141
|
+
};
|
|
142
|
+
this.storageProvider.setItem(this.getColumnStorageKey(pRecordSet, pViewContext), JSON.stringify(tmpColumnMeta));
|
|
143
|
+
return tmpOverrides;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Clear all column visibility overrides for a record set (Reset to defaults).
|
|
148
|
+
*
|
|
149
|
+
* @param {string} pRecordSet - The record set to clear overrides for
|
|
150
|
+
* @param {string} [pViewContext] - The view context (defaults to 'List')
|
|
151
|
+
* @return {boolean} True when the overrides have been cleared.
|
|
152
|
+
*/
|
|
153
|
+
clearColumnVisibilityOverrides(pRecordSet, pViewContext)
|
|
154
|
+
{
|
|
155
|
+
this.storageProvider.removeItem(this.getColumnStorageKey(pRecordSet, pViewContext));
|
|
156
|
+
if (this.pict.Bundle._ActiveColumnState && this.pict.Bundle._ActiveColumnState[pRecordSet])
|
|
157
|
+
{
|
|
158
|
+
delete this.pict.Bundle._ActiveColumnState[pRecordSet];
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Write the session mirror of a record set's override map into the Bundle.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} pRecordSet - The record set the overrides belong to
|
|
167
|
+
* @param {Record<string, boolean>} pOverrides - The override map to mirror
|
|
168
|
+
*/
|
|
169
|
+
_seedBundleColumnState(pRecordSet, pOverrides)
|
|
170
|
+
{
|
|
171
|
+
if (!this.pict.Bundle._ActiveColumnState)
|
|
172
|
+
{
|
|
173
|
+
this.pict.Bundle._ActiveColumnState = {};
|
|
174
|
+
}
|
|
175
|
+
this.pict.Bundle._ActiveColumnState[pRecordSet] = { Overrides: pOverrides };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** ===== SIMPLE KEY-VALUE CACHE ============= */
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {string} pKey - The key to get from the cache
|
|
182
|
+
* @return {any} - The value associated with the key, or null if not found
|
|
183
|
+
*/
|
|
184
|
+
getItem(pKey)
|
|
185
|
+
{
|
|
186
|
+
if (pKey in this.keyCache)
|
|
187
|
+
{
|
|
188
|
+
return this.keyCache[pKey];
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {string} pKey - The key to set in the cache
|
|
195
|
+
* @param {any} pValue - The value to associate with the key
|
|
196
|
+
*/
|
|
197
|
+
setItem(pKey, pValue)
|
|
198
|
+
{
|
|
199
|
+
this.keyCache[pKey] = pValue;
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {string} pKey - The key to remove from the cache
|
|
205
|
+
* @return {boolean} - True if the item was removed, false if it was not found
|
|
206
|
+
*/
|
|
207
|
+
removeItem(pKey)
|
|
208
|
+
{
|
|
209
|
+
if (pKey in this.keyCache)
|
|
210
|
+
{
|
|
211
|
+
delete this.keyCache[pKey];
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = ColumnDataProvider;
|
|
219
|
+
module.exports.default_configuration = _DEFAULT_PROVIDER_CONFIGURATION;
|
|
@@ -316,6 +316,12 @@ class RecordSetProviderBase extends libPictProvider
|
|
|
316
316
|
tmpClause = JSON.parse(JSON.stringify(tmpClause));
|
|
317
317
|
tmpClause.Hash = `${pFilterKey}-${pClauseKey}-${this.pict.getUUID()}`;
|
|
318
318
|
tmpClause.Label = tmpClause.Label || tmpClause.DisplayName;
|
|
319
|
+
// Stamp the owning recordset so per-clause re-renders (which rebuild the render record
|
|
320
|
+
// from the live clause alone) can still resolve their provider and remove control.
|
|
321
|
+
if (!tmpClause.RecordSet && this.options.RecordSet)
|
|
322
|
+
{
|
|
323
|
+
tmpClause.RecordSet = this.options.RecordSet;
|
|
324
|
+
}
|
|
319
325
|
const tmpClauses = this.getFilterClauses();
|
|
320
326
|
tmpClauses.push(tmpClause);
|
|
321
327
|
}
|
|
@@ -467,7 +473,8 @@ class RecordSetProviderBase extends libPictProvider
|
|
|
467
473
|
*/
|
|
468
474
|
_pickQuickClause(pAvailableClauses)
|
|
469
475
|
{
|
|
470
|
-
return pAvailableClauses.find((pClause) => pClause.Type === '
|
|
476
|
+
return pAvailableClauses.find((pClause) => pClause.Type === 'DistinctSelectedValueList')
|
|
477
|
+
|| pAvailableClauses.find((pClause) => pClause.Type === 'InternalJoinSelectedValue' || pClause.Type === 'InternalJoinSelectedValueList')
|
|
471
478
|
|| pAvailableClauses.find((pClause) => pClause.Type === 'StringMatch' && pClause.ExactMatch === false)
|
|
472
479
|
|| pAvailableClauses.find((pClause) => pClause.Type === 'DateRange')
|
|
473
480
|
|| pAvailableClauses.find((pClause) => pClause.Type === 'NumericRange')
|
|
@@ -484,12 +491,64 @@ class RecordSetProviderBase extends libPictProvider
|
|
|
484
491
|
{
|
|
485
492
|
case 'StringMatch': return 'text';
|
|
486
493
|
case 'DateRange': return 'daterange';
|
|
494
|
+
case 'DistinctSelectedValueList': return 'distinct';
|
|
487
495
|
case 'InternalJoinSelectedValue':
|
|
488
496
|
case 'InternalJoinSelectedValueList': return 'entity';
|
|
489
497
|
default: return null;
|
|
490
498
|
}
|
|
491
499
|
}
|
|
492
500
|
|
|
501
|
+
/**
|
|
502
|
+
* The entity's soft-delete column name. Subclasses with schema access override this
|
|
503
|
+
* (the Meadow provider resolves the column whose Type is 'Deleted').
|
|
504
|
+
* @return {string}
|
|
505
|
+
*/
|
|
506
|
+
getDeletedField()
|
|
507
|
+
{
|
|
508
|
+
return 'Deleted';
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** @return {boolean} Whether the show-deleted clause is currently active for this record set. */
|
|
512
|
+
getShowDeletedFilterValue()
|
|
513
|
+
{
|
|
514
|
+
return this.getFilterClauses().some((pClause) => pClause.ShowDeletedKey === 'ShowDeleted');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Toggle the show-deleted clause: a RawFilter stanza that references the Deleted column
|
|
519
|
+
* explicitly, which suppresses the automatic `Deleted = 0` delete tracking so soft-deleted
|
|
520
|
+
* rows enumerate. It is a real clause in the active filter state, so it serializes into the
|
|
521
|
+
* filter experience (and the route URL) and clears with Clear like any other clause. The
|
|
522
|
+
* clause list UI skips it (no filter view for RawFilter) — the drawer checkbox is its face.
|
|
523
|
+
*
|
|
524
|
+
* @param {boolean} pOn - Whether deleted records should be included.
|
|
525
|
+
* @return {boolean} The resulting state.
|
|
526
|
+
*/
|
|
527
|
+
setShowDeletedFilterValue(pOn)
|
|
528
|
+
{
|
|
529
|
+
const tmpClauses = this.getFilterClauses();
|
|
530
|
+
const tmpIndex = tmpClauses.findIndex((pClause) => pClause.ShowDeletedKey === 'ShowDeleted');
|
|
531
|
+
if (pOn === true)
|
|
532
|
+
{
|
|
533
|
+
if (tmpIndex < 0)
|
|
534
|
+
{
|
|
535
|
+
tmpClauses.push(
|
|
536
|
+
{
|
|
537
|
+
Type: 'RawFilter',
|
|
538
|
+
ShowDeletedKey: 'ShowDeleted',
|
|
539
|
+
Label: 'Show deleted',
|
|
540
|
+
Value: `FBL~${this.getDeletedField()}~INN~0,1`,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
if (tmpIndex >= 0)
|
|
546
|
+
{
|
|
547
|
+
tmpClauses.splice(tmpIndex, 1);
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
|
|
493
552
|
/** @param {string} pField @return {any} The current value of a field's quick-filter clause, or ''. */
|
|
494
553
|
getQuickFilterClauseValue(pField)
|
|
495
554
|
{
|
|
@@ -617,6 +676,10 @@ class RecordSetProviderBase extends libPictProvider
|
|
|
617
676
|
tmpClause.Label = tmpClause.Label || tmpClause.DisplayName;
|
|
618
677
|
tmpClause.QuickFilter = true;
|
|
619
678
|
tmpClause.QuickFilterKey = pQuickFilterKey;
|
|
679
|
+
if (!tmpClause.RecordSet && this.options.RecordSet)
|
|
680
|
+
{
|
|
681
|
+
tmpClause.RecordSet = this.options.RecordSet;
|
|
682
|
+
}
|
|
620
683
|
return tmpClause;
|
|
621
684
|
}
|
|
622
685
|
|
|
@@ -68,35 +68,50 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Fetch (and cache) the DISTINCT values of a column present in this recordset's data, via
|
|
71
|
-
* Meadow's `<Entity>s/Distinct/<Column>` endpoint. Drives the `ScopeToRecordSet` filter knob
|
|
72
|
-
* an entity picker
|
|
73
|
-
*
|
|
71
|
+
* Meadow's `<Entity>s/Distinct/<Column>` endpoint. Drives the `ScopeToRecordSet` filter knob
|
|
72
|
+
* (an entity picker limited to `FBL~<Column>~INN~<these values>`) and the
|
|
73
|
+
* `DistinctSelectedValueList` filter type (a dropdown whose options ARE these values).
|
|
74
|
+
* Cached per column; the cache is cleared on create/update/delete through this provider.
|
|
74
75
|
*
|
|
75
|
-
* @param {string} pColumn
|
|
76
|
+
* @param {string} pColumn
|
|
77
|
+
* @param {{ Filter?: string } | ((pError: Error|null, pValues: Array<any>) => void)} [pOptions] - Optional; `Filter` is a FoxHound stanza appended as `/FilteredTo/<Filter>` on the distinct query.
|
|
78
|
+
* @param {(pError: Error|null, pValues: Array<any>) => void} [fCallback]
|
|
76
79
|
*/
|
|
77
|
-
getRecordSetColumnDistinct(pColumn, fCallback)
|
|
80
|
+
getRecordSetColumnDistinct(pColumn, pOptions, fCallback)
|
|
78
81
|
{
|
|
82
|
+
// Back-compat: (pColumn, fCallback)
|
|
83
|
+
let tmpOptions = pOptions;
|
|
84
|
+
let tmpCallback = fCallback;
|
|
85
|
+
if (typeof tmpOptions === 'function')
|
|
86
|
+
{
|
|
87
|
+
tmpCallback = tmpOptions;
|
|
88
|
+
tmpOptions = {};
|
|
89
|
+
}
|
|
90
|
+
tmpOptions = tmpOptions || {};
|
|
91
|
+
// Unfiltered fetches keep the bare-column key (synchronous peeks elsewhere read it);
|
|
92
|
+
// filtered fetches get their own key so the two never cross-pollinate.
|
|
93
|
+
const tmpCacheKey = tmpOptions.Filter ? `${pColumn}::${tmpOptions.Filter}` : pColumn;
|
|
79
94
|
this._scopeDistinctCache = this._scopeDistinctCache || {};
|
|
80
|
-
if (Array.isArray(this._scopeDistinctCache[
|
|
95
|
+
if (Array.isArray(this._scopeDistinctCache[tmpCacheKey]))
|
|
81
96
|
{
|
|
82
|
-
return
|
|
97
|
+
return tmpCallback(null, this._scopeDistinctCache[tmpCacheKey]);
|
|
83
98
|
}
|
|
84
99
|
if (!this.options.Entity || !this.entityProvider || !this.entityProvider.restClient)
|
|
85
100
|
{
|
|
86
|
-
return
|
|
101
|
+
return tmpCallback(new Error('RecordSet provider cannot resolve a distinct request (missing Entity or rest client).'), []);
|
|
87
102
|
}
|
|
88
|
-
const tmpURL = `${this.options.URLPrefix || ''}${this.options.Entity}s/Distinct/${pColumn}`;
|
|
103
|
+
const tmpURL = `${this.options.URLPrefix || ''}${this.options.Entity}s/Distinct/${pColumn}${tmpOptions.Filter ? `/FilteredTo/${tmpOptions.Filter}` : ''}`;
|
|
89
104
|
this.entityProvider.restClient.getJSON(tmpURL, (pError, pResponse, pBody) =>
|
|
90
105
|
{
|
|
91
106
|
if (pError || (pResponse && pResponse.statusCode > 299) || !Array.isArray(pBody))
|
|
92
107
|
{
|
|
93
108
|
this.pict.log.warn(`RecordSet [${this.options.RecordSet || this.options.Entity}] distinct fetch for [${pColumn}] failed; the scoped filter falls back to unscoped.`, { Error: pError && pError.message, URL: tmpURL });
|
|
94
|
-
this._scopeDistinctCache[
|
|
95
|
-
return
|
|
109
|
+
this._scopeDistinctCache[tmpCacheKey] = [];
|
|
110
|
+
return tmpCallback(pError || new Error('distinct fetch returned a non-array'), []);
|
|
96
111
|
}
|
|
97
112
|
const tmpValues = [ ...new Set(pBody.map((pRecord) => pRecord && pRecord[pColumn]).filter((pValue) => pValue != null)) ];
|
|
98
|
-
this._scopeDistinctCache[
|
|
99
|
-
return
|
|
113
|
+
this._scopeDistinctCache[tmpCacheKey] = tmpValues;
|
|
114
|
+
return tmpCallback(null, tmpValues);
|
|
100
115
|
});
|
|
101
116
|
}
|
|
102
117
|
|
|
@@ -161,7 +176,7 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
161
176
|
}
|
|
162
177
|
|
|
163
178
|
getIDField()
|
|
164
|
-
{
|
|
179
|
+
{
|
|
165
180
|
if (this._Schema?.MeadowSchema?.Schema?.length)
|
|
166
181
|
{
|
|
167
182
|
for (let field of this._Schema.MeadowSchema.Schema)
|
|
@@ -175,12 +190,29 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
175
190
|
return `ID${ this.options.Entity }`;
|
|
176
191
|
}
|
|
177
192
|
|
|
193
|
+
getDeletedField()
|
|
194
|
+
{
|
|
195
|
+
if (this._Schema?.MeadowSchema?.Schema?.length)
|
|
196
|
+
{
|
|
197
|
+
for (let field of this._Schema.MeadowSchema.Schema)
|
|
198
|
+
{
|
|
199
|
+
if (field.Type == 'Deleted')
|
|
200
|
+
{
|
|
201
|
+
return field.Column;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return 'Deleted';
|
|
206
|
+
}
|
|
207
|
+
|
|
178
208
|
/**
|
|
179
209
|
* Get a record by its ID or GUID.
|
|
180
210
|
*
|
|
181
211
|
* @param {string|number} pGuid - The ID or GUID of the record.
|
|
212
|
+
* @param {boolean} [pIncludeDeleted] - When true, also match soft-deleted records (the explicit
|
|
213
|
+
* Deleted filter suppresses the automatic `Deleted = 0`).
|
|
182
214
|
*/
|
|
183
|
-
async getRecordByGUID(pGuid)
|
|
215
|
+
async getRecordByGUID(pGuid, pIncludeDeleted)
|
|
184
216
|
{
|
|
185
217
|
if (!this.options.Entity)
|
|
186
218
|
{
|
|
@@ -190,9 +222,14 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
190
222
|
{
|
|
191
223
|
this.pict.log.info(`Reading ${this.options.Entity} record by GUID`, { GUID: pGuid });
|
|
192
224
|
}
|
|
225
|
+
let tmpFilterString = `FBV~${ this.getGUIDField() }~EQ~${encodeURIComponent(pGuid)}`;
|
|
226
|
+
if (pIncludeDeleted === true)
|
|
227
|
+
{
|
|
228
|
+
tmpFilterString += `~FBL~${ this.getDeletedField() }~INN~0,1`;
|
|
229
|
+
}
|
|
193
230
|
return new Promise((resolve, reject) =>
|
|
194
231
|
{
|
|
195
|
-
this.entityProvider.getEntitySet(this.options.Entity,
|
|
232
|
+
this.entityProvider.getEntitySet(this.options.Entity, tmpFilterString, (pError, pResult) =>
|
|
196
233
|
{
|
|
197
234
|
if (pError)
|
|
198
235
|
{
|
|
@@ -223,6 +260,9 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
223
260
|
{
|
|
224
261
|
tmpClauses.push({ Type: 'RawFilter', Value: pOptions.FilterString });
|
|
225
262
|
}
|
|
263
|
+
// (The show-deleted switch needs no special handling here: it is a real RawFilter clause in
|
|
264
|
+
// the active filter state — see setShowDeletedFilterValue on the base provider — so it rides
|
|
265
|
+
// the FilterClauses concat above into both the records and count fetches.)
|
|
226
266
|
return [ tmpClauses, tmpExperience ];
|
|
227
267
|
}
|
|
228
268
|
|
|
@@ -280,6 +320,33 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
280
320
|
tmpColumns.push(tmpKey);
|
|
281
321
|
}
|
|
282
322
|
}
|
|
323
|
+
// Column chooser: union in user-shown schema-tier columns so a toggled-on column's data is
|
|
324
|
+
// actually fetched. Same gauntlet as the descriptors; gated on the config flag so stale
|
|
325
|
+
// stored overrides can never widen queries for lists that don't use the chooser.
|
|
326
|
+
if (tmpConfig && tmpConfig.RecordSetListColumnChooser === true && this.pict.providers.ColumnDataProvider)
|
|
327
|
+
{
|
|
328
|
+
const tmpRecordSet = (pOptions && pOptions.RecordSet) || tmpConfig.RecordSet || pEntity;
|
|
329
|
+
const tmpOverrides = this.pict.providers.ColumnDataProvider.getColumnVisibilityOverrides(tmpRecordSet, 'List');
|
|
330
|
+
for (const tmpKey of Object.keys(tmpOverrides))
|
|
331
|
+
{
|
|
332
|
+
if (tmpOverrides[tmpKey] !== true)
|
|
333
|
+
{
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (tmpKey.startsWith('ID') || tmpKey.startsWith('GUID') || tmpKey === 'CreatingIDUser' || tmpKey === 'UpdateDate')
|
|
337
|
+
{
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (!(tmpKey in tmpColumnType) || tmpBlobTypes[tmpColumnType[tmpKey]])
|
|
341
|
+
{
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (!tmpColumns.includes(tmpKey))
|
|
345
|
+
{
|
|
346
|
+
tmpColumns.push(tmpKey);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
283
350
|
return tmpColumns;
|
|
284
351
|
}
|
|
285
352
|
|
|
@@ -495,6 +562,9 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
495
562
|
}
|
|
496
563
|
// A new record changes the total; drop the cached count so the next render re-counts.
|
|
497
564
|
this._RecordSetCountCache = null;
|
|
565
|
+
// Mutations can introduce/retire column values; drop the distinct cache so
|
|
566
|
+
// ScopeToRecordSet scoping and DistinctSelectedValueList dropdowns refresh.
|
|
567
|
+
this._scopeDistinctCache = null;
|
|
498
568
|
// Drop this list's scoped cache too, so the next render re-fetches fresh.
|
|
499
569
|
if (typeof this.pict.EntityProvider.clearScope === 'function')
|
|
500
570
|
{
|
|
@@ -529,6 +599,9 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
529
599
|
}
|
|
530
600
|
// An edit can move a record in or out of the active filter; drop the cached count to be safe.
|
|
531
601
|
this._RecordSetCountCache = null;
|
|
602
|
+
// Mutations can introduce/retire column values; drop the distinct cache so
|
|
603
|
+
// ScopeToRecordSet scoping and DistinctSelectedValueList dropdowns refresh.
|
|
604
|
+
this._scopeDistinctCache = null;
|
|
532
605
|
// Drop this list's scoped cache too, so the next render re-fetches fresh.
|
|
533
606
|
if (typeof this.pict.EntityProvider.clearScope === 'function')
|
|
534
607
|
{
|
|
@@ -563,6 +636,9 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
563
636
|
}
|
|
564
637
|
// A delete changes the total; drop the cached count so the next render re-counts.
|
|
565
638
|
this._RecordSetCountCache = null;
|
|
639
|
+
// Mutations can introduce/retire column values; drop the distinct cache so
|
|
640
|
+
// ScopeToRecordSet scoping and DistinctSelectedValueList dropdowns refresh.
|
|
641
|
+
this._scopeDistinctCache = null;
|
|
566
642
|
// Drop this list's scoped cache too, so the next render re-fetches fresh.
|
|
567
643
|
if (typeof this.pict.EntityProvider.clearScope === 'function')
|
|
568
644
|
{
|
|
@@ -11,6 +11,7 @@ const ProviderBase = require('../providers/RecordSet-RecordProvider-Base.js');
|
|
|
11
11
|
const ProviderMeadowEndpoints = require('../providers/RecordSet-RecordProvider-MeadowEndpoints.js');
|
|
12
12
|
|
|
13
13
|
const ProviderLinkManager = require('../providers/RecordSet-Link-Manager.js');
|
|
14
|
+
const libProviderColumnData = require('../providers/Column-Data-Provider.js');
|
|
14
15
|
|
|
15
16
|
const ProviderRouter = require('../providers/RecordSet-Router.js');
|
|
16
17
|
|
|
@@ -101,9 +102,20 @@ class RecordSetMetacontroller extends libFableServiceProviderBase
|
|
|
101
102
|
resolve(pResult);
|
|
102
103
|
});
|
|
103
104
|
});
|
|
105
|
+
// A dangling reference (the target row no longer exists — e.g. a system user id that
|
|
106
|
+
// was never seeded) comes back as the endpoint's error body rather than an entity.
|
|
107
|
+
// Render the raw id, unlinked: it carries more information than a blank cell, and
|
|
108
|
+
// matches how a found-but-nameless entity already renders.
|
|
109
|
+
if (!entity || (entity.Error && entity.StatusCode))
|
|
110
|
+
{
|
|
111
|
+
return fCallback(null, value);
|
|
112
|
+
}
|
|
104
113
|
if (remote === 'User')
|
|
105
114
|
{
|
|
106
|
-
|
|
115
|
+
// Compose what the user record actually has; fall back through the common
|
|
116
|
+
// name fields rather than printing "undefined undefined".
|
|
117
|
+
const tmpUserName = [ entity.NameFirst, entity.NameLast ].filter((pPart) => (typeof(pPart) === 'string') && pPart.trim().length > 0).join(' ');
|
|
118
|
+
value = tmpUserName || entity.FullName || entity.Name || entity.LoginID || value;
|
|
107
119
|
}
|
|
108
120
|
else if (entity?.Name)
|
|
109
121
|
{
|
|
@@ -440,6 +452,13 @@ class RecordSetMetacontroller extends libFableServiceProviderBase
|
|
|
440
452
|
|
|
441
453
|
this.fable.addProvider('RecordSetLinkManager', {}, ProviderLinkManager);
|
|
442
454
|
|
|
455
|
+
// Column visibility persistence — only register the built-in localStorage provider when the
|
|
456
|
+
// host hasn't supplied its own (the documented seam for server-side per-user persistence).
|
|
457
|
+
if (!('ColumnDataProvider' in this.fable.providers))
|
|
458
|
+
{
|
|
459
|
+
this.fable.addProvider('ColumnDataProvider', libProviderColumnData.default_configuration, libProviderColumnData);
|
|
460
|
+
}
|
|
461
|
+
|
|
443
462
|
// Add the subviews internally and externally
|
|
444
463
|
this.pict.addTemplate(require('../templates/Pict-Template-FilterView.js'));
|
|
445
464
|
this.pict.addTemplate(require('../templates/Pict-Template-FilterInstanceViews.js'));
|
|
@@ -583,6 +602,9 @@ class RecordSetMetacontroller extends libFableServiceProviderBase
|
|
|
583
602
|
DisplayName: descriptor.Name || key,
|
|
584
603
|
ManifestHash: pManifest.Scope,
|
|
585
604
|
PictDashboard: tmpPictDashboard,
|
|
605
|
+
// Column-chooser default visibility: a descriptor can ship hidden-by-default and be
|
|
606
|
+
// turned on by the user (when the recordset enables RecordSetListColumnChooser).
|
|
607
|
+
DefaultHidden: (descriptor.DefaultHidden === true),
|
|
586
608
|
};
|
|
587
609
|
});
|
|
588
610
|
pManifest.TableCells = tmpTableCells;
|
|
@@ -78,6 +78,10 @@ class PictTemplateFilterInstanceViewInstruction extends libPictTemplate
|
|
|
78
78
|
// not the drawer's clause list — skip them here so they aren't rendered twice. Both the sync and
|
|
79
79
|
// async render loops treat a null view as "skip this clause".
|
|
80
80
|
if (pClause && pClause.QuickFilter) { return null; }
|
|
81
|
+
// The show-deleted switch is a RawFilter clause whose face is the drawer-footer checkbox
|
|
82
|
+
// (RecordSet-Filters._renderShowDeletedControl) — without this skip it would fall through to
|
|
83
|
+
// the Base filter card, which renders a debug dump for types with no real filter view.
|
|
84
|
+
if (pClause && pClause.ShowDeletedKey) { return null; }
|
|
81
85
|
let tmpViewHash = `PRSP-FilterType-${pClause.Type}`;
|
|
82
86
|
const tmpCustomViewHash = `${tmpViewHash}-${pClause.CustomFilterViewHash}`;
|
|
83
87
|
if (tmpCustomViewHash in this.pict.views)
|