pict-section-recordset 1.9.6 → 1.10.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 +1 -1
- package/source/Pict-Section-RecordSet.js +1 -0
- package/source/providers/Column-Data-Provider.js +219 -0
- package/source/providers/RecordSet-RecordProvider-Base.js +51 -0
- package/source/providers/RecordSet-RecordProvider-MeadowEndpoints.js +55 -3
- package/source/services/RecordsSet-MetaController.js +23 -1
- package/source/templates/Pict-Template-FilterInstanceViews.js +4 -0
- package/source/views/RecordSet-Filters.js +54 -1
- 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
|
@@ -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;
|
|
@@ -490,6 +490,57 @@ class RecordSetProviderBase extends libPictProvider
|
|
|
490
490
|
}
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
+
/**
|
|
494
|
+
* The entity's soft-delete column name. Subclasses with schema access override this
|
|
495
|
+
* (the Meadow provider resolves the column whose Type is 'Deleted').
|
|
496
|
+
* @return {string}
|
|
497
|
+
*/
|
|
498
|
+
getDeletedField()
|
|
499
|
+
{
|
|
500
|
+
return 'Deleted';
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** @return {boolean} Whether the show-deleted clause is currently active for this record set. */
|
|
504
|
+
getShowDeletedFilterValue()
|
|
505
|
+
{
|
|
506
|
+
return this.getFilterClauses().some((pClause) => pClause.ShowDeletedKey === 'ShowDeleted');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Toggle the show-deleted clause: a RawFilter stanza that references the Deleted column
|
|
511
|
+
* explicitly, which suppresses the automatic `Deleted = 0` delete tracking so soft-deleted
|
|
512
|
+
* rows enumerate. It is a real clause in the active filter state, so it serializes into the
|
|
513
|
+
* filter experience (and the route URL) and clears with Clear like any other clause. The
|
|
514
|
+
* clause list UI skips it (no filter view for RawFilter) — the drawer checkbox is its face.
|
|
515
|
+
*
|
|
516
|
+
* @param {boolean} pOn - Whether deleted records should be included.
|
|
517
|
+
* @return {boolean} The resulting state.
|
|
518
|
+
*/
|
|
519
|
+
setShowDeletedFilterValue(pOn)
|
|
520
|
+
{
|
|
521
|
+
const tmpClauses = this.getFilterClauses();
|
|
522
|
+
const tmpIndex = tmpClauses.findIndex((pClause) => pClause.ShowDeletedKey === 'ShowDeleted');
|
|
523
|
+
if (pOn === true)
|
|
524
|
+
{
|
|
525
|
+
if (tmpIndex < 0)
|
|
526
|
+
{
|
|
527
|
+
tmpClauses.push(
|
|
528
|
+
{
|
|
529
|
+
Type: 'RawFilter',
|
|
530
|
+
ShowDeletedKey: 'ShowDeleted',
|
|
531
|
+
Label: 'Show deleted',
|
|
532
|
+
Value: `FBL~${this.getDeletedField()}~INN~0,1`,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
if (tmpIndex >= 0)
|
|
538
|
+
{
|
|
539
|
+
tmpClauses.splice(tmpIndex, 1);
|
|
540
|
+
}
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
|
|
493
544
|
/** @param {string} pField @return {any} The current value of a field's quick-filter clause, or ''. */
|
|
494
545
|
getQuickFilterClauseValue(pField)
|
|
495
546
|
{
|
|
@@ -161,7 +161,7 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
getIDField()
|
|
164
|
-
{
|
|
164
|
+
{
|
|
165
165
|
if (this._Schema?.MeadowSchema?.Schema?.length)
|
|
166
166
|
{
|
|
167
167
|
for (let field of this._Schema.MeadowSchema.Schema)
|
|
@@ -175,12 +175,29 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
175
175
|
return `ID${ this.options.Entity }`;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
getDeletedField()
|
|
179
|
+
{
|
|
180
|
+
if (this._Schema?.MeadowSchema?.Schema?.length)
|
|
181
|
+
{
|
|
182
|
+
for (let field of this._Schema.MeadowSchema.Schema)
|
|
183
|
+
{
|
|
184
|
+
if (field.Type == 'Deleted')
|
|
185
|
+
{
|
|
186
|
+
return field.Column;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return 'Deleted';
|
|
191
|
+
}
|
|
192
|
+
|
|
178
193
|
/**
|
|
179
194
|
* Get a record by its ID or GUID.
|
|
180
195
|
*
|
|
181
196
|
* @param {string|number} pGuid - The ID or GUID of the record.
|
|
197
|
+
* @param {boolean} [pIncludeDeleted] - When true, also match soft-deleted records (the explicit
|
|
198
|
+
* Deleted filter suppresses the automatic `Deleted = 0`).
|
|
182
199
|
*/
|
|
183
|
-
async getRecordByGUID(pGuid)
|
|
200
|
+
async getRecordByGUID(pGuid, pIncludeDeleted)
|
|
184
201
|
{
|
|
185
202
|
if (!this.options.Entity)
|
|
186
203
|
{
|
|
@@ -190,9 +207,14 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
190
207
|
{
|
|
191
208
|
this.pict.log.info(`Reading ${this.options.Entity} record by GUID`, { GUID: pGuid });
|
|
192
209
|
}
|
|
210
|
+
let tmpFilterString = `FBV~${ this.getGUIDField() }~EQ~${encodeURIComponent(pGuid)}`;
|
|
211
|
+
if (pIncludeDeleted === true)
|
|
212
|
+
{
|
|
213
|
+
tmpFilterString += `~FBL~${ this.getDeletedField() }~INN~0,1`;
|
|
214
|
+
}
|
|
193
215
|
return new Promise((resolve, reject) =>
|
|
194
216
|
{
|
|
195
|
-
this.entityProvider.getEntitySet(this.options.Entity,
|
|
217
|
+
this.entityProvider.getEntitySet(this.options.Entity, tmpFilterString, (pError, pResult) =>
|
|
196
218
|
{
|
|
197
219
|
if (pError)
|
|
198
220
|
{
|
|
@@ -223,6 +245,9 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
223
245
|
{
|
|
224
246
|
tmpClauses.push({ Type: 'RawFilter', Value: pOptions.FilterString });
|
|
225
247
|
}
|
|
248
|
+
// (The show-deleted switch needs no special handling here: it is a real RawFilter clause in
|
|
249
|
+
// the active filter state — see setShowDeletedFilterValue on the base provider — so it rides
|
|
250
|
+
// the FilterClauses concat above into both the records and count fetches.)
|
|
226
251
|
return [ tmpClauses, tmpExperience ];
|
|
227
252
|
}
|
|
228
253
|
|
|
@@ -280,6 +305,33 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
280
305
|
tmpColumns.push(tmpKey);
|
|
281
306
|
}
|
|
282
307
|
}
|
|
308
|
+
// Column chooser: union in user-shown schema-tier columns so a toggled-on column's data is
|
|
309
|
+
// actually fetched. Same gauntlet as the descriptors; gated on the config flag so stale
|
|
310
|
+
// stored overrides can never widen queries for lists that don't use the chooser.
|
|
311
|
+
if (tmpConfig && tmpConfig.RecordSetListColumnChooser === true && this.pict.providers.ColumnDataProvider)
|
|
312
|
+
{
|
|
313
|
+
const tmpRecordSet = (pOptions && pOptions.RecordSet) || tmpConfig.RecordSet || pEntity;
|
|
314
|
+
const tmpOverrides = this.pict.providers.ColumnDataProvider.getColumnVisibilityOverrides(tmpRecordSet, 'List');
|
|
315
|
+
for (const tmpKey of Object.keys(tmpOverrides))
|
|
316
|
+
{
|
|
317
|
+
if (tmpOverrides[tmpKey] !== true)
|
|
318
|
+
{
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (tmpKey.startsWith('ID') || tmpKey.startsWith('GUID') || tmpKey === 'CreatingIDUser' || tmpKey === 'UpdateDate')
|
|
322
|
+
{
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (!(tmpKey in tmpColumnType) || tmpBlobTypes[tmpColumnType[tmpKey]])
|
|
326
|
+
{
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (!tmpColumns.includes(tmpKey))
|
|
330
|
+
{
|
|
331
|
+
tmpColumns.push(tmpKey);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
283
335
|
return tmpColumns;
|
|
284
336
|
}
|
|
285
337
|
|
|
@@ -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)
|
|
@@ -88,6 +88,12 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
|
|
|
88
88
|
.prsp-quickfilter-dash { color: var(--theme-color-text-muted, #6b7686); }
|
|
89
89
|
/* Entity quick control: the pict-section-picker mounts into this host (its own .pps chrome themes it). */
|
|
90
90
|
.prsp-quickfilter-entityhost { display: inline-block; width: 14rem; max-width: 100%; vertical-align: middle; }
|
|
91
|
+
/* Show-deleted switch (RecordSetListShowDeletedFilter): a labeled checkbox in the drawer actions row.
|
|
92
|
+
Painted post-render into the host label; :empty hides it for record sets that haven't opted in. */
|
|
93
|
+
.prsp-filters-showdeleted { display: inline-flex; align-items: center; gap: 0.4rem; cursor: pointer; user-select: none;
|
|
94
|
+
font-size: 0.88rem; color: var(--theme-color-text-secondary, #45505f); margin-right: 0.35rem; }
|
|
95
|
+
.prsp-filters-showdeleted:empty { display: none; }
|
|
96
|
+
.prsp-filters-showdeleted-checkbox { width: 1.05rem; height: 1.05rem; cursor: pointer; accent-color: var(--theme-color-brand-primary, #156dd1); }
|
|
91
97
|
|
|
92
98
|
/* Module-owned "Add filter" popover (replaces the old native <select> pickers). */
|
|
93
99
|
/* Fixed (viewport-anchored) + JS-positioned on open, so no ancestor overflow:hidden — the filter card,
|
|
@@ -196,6 +202,7 @@ const _DEFAULT_CONFIGURATION_SUBSET_Filter =
|
|
|
196
202
|
Template: /*html*/`
|
|
197
203
|
<!-- DefaultPackage pict view template: [PRSP-SUBSET-Filters-Template-DrawerActions-Fieldset] -->
|
|
198
204
|
<div class="prsp-filters-actions">
|
|
205
|
+
<label class="prsp-filters-showdeleted" id="PRSP_ShowDeleted_Host" title="Include soft-deleted records in the results"></label>
|
|
199
206
|
<button type="button" class="prsp-filters-btn-text" id="PRSP_Filter_Button_Clear" title="Clear all filters to a blank state" onclick="_Pict.views['PRSP-Filters'].handleClear(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}')">Clear</button>
|
|
200
207
|
<button type="button" class="prsp-filters-btn-text" id="PRSP_Filter_Button_Reset" title="Reset all filters to the last saved/defaulted state" onclick="_Pict.views['PRSP-Filters'].handleReset(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}')">Reset</button>
|
|
201
208
|
<button type="button" class="prsp-filters-apply" id="PRSP_Filter_Button_ApplyDrawer" onclick="_Pict.views['PRSP-Filters'].handleSearch(event, '{~D:Record.RecordSet~}', '{~D:Record.ViewContext~}')">Apply</button>
|
|
@@ -719,6 +726,51 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
719
726
|
*
|
|
720
727
|
* @param {string} pRecordSet @param {string} pViewContext @param {string} pField @param {string} pClauseKey @param {string} pValue
|
|
721
728
|
*/
|
|
729
|
+
/**
|
|
730
|
+
* Flip the show-deleted switch (the drawer-footer checkbox, RecordSetListShowDeletedFilter
|
|
731
|
+
* recordsets). The switch is a REAL clause — a RawFilter referencing the Deleted column, which
|
|
732
|
+
* suppresses the automatic `Deleted = 0` so soft-deleted rows enumerate — upserted into the
|
|
733
|
+
* active filter state and applied through the normal search flow. Because the clause changes
|
|
734
|
+
* the serialized filter experience, the route URL always changes: the fetch reliably fires,
|
|
735
|
+
* and the state survives reloads and shared links. Clear/Reset drop it like any clause.
|
|
736
|
+
*
|
|
737
|
+
* @param {string} pRecordSet - The record set the toggle belongs to
|
|
738
|
+
* @param {string} pViewContext - The view context (List, Dashboard)
|
|
739
|
+
* @param {boolean} pChecked - Whether deleted records should be included
|
|
740
|
+
*/
|
|
741
|
+
toggleShowDeletedFilter(pRecordSet, pViewContext, pChecked)
|
|
742
|
+
{
|
|
743
|
+
const tmpProvider = this.pict.providers['RSP-Provider-' + pRecordSet];
|
|
744
|
+
if (!tmpProvider || typeof tmpProvider.setShowDeletedFilterValue !== 'function')
|
|
745
|
+
{
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
tmpProvider.setShowDeletedFilterValue(pChecked === true);
|
|
749
|
+
this.handleSearch(null, pRecordSet, pViewContext);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Paint the show-deleted checkbox into its drawer-footer host (next to Clear/Reset/Apply),
|
|
754
|
+
* seeded from the clause's presence. Painted post-render like the quick bar; empty when the
|
|
755
|
+
* record set hasn't opted in via RecordSetListShowDeletedFilter.
|
|
756
|
+
*
|
|
757
|
+
* @param {string} pRecordSet @param {string} pViewContext
|
|
758
|
+
*/
|
|
759
|
+
_renderShowDeletedControl(pRecordSet, pViewContext)
|
|
760
|
+
{
|
|
761
|
+
if (!document.getElementById('PRSP_ShowDeleted_Host')) { return; }
|
|
762
|
+
const tmpRecordSetConfiguration = this.pict.PictSectionRecordSet?.recordSetProviderConfigurations?.[pRecordSet] || {};
|
|
763
|
+
if (tmpRecordSetConfiguration.RecordSetListShowDeletedFilter !== true)
|
|
764
|
+
{
|
|
765
|
+
this.pict.ContentAssignment.assignContent('#PRSP_ShowDeleted_Host', '');
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const tmpProvider = this.pict.providers['RSP-Provider-' + pRecordSet];
|
|
769
|
+
const tmpChecked = (tmpProvider && typeof tmpProvider.getShowDeletedFilterValue === 'function' && tmpProvider.getShowDeletedFilterValue()) ? 'checked' : '';
|
|
770
|
+
this.pict.ContentAssignment.assignContent('#PRSP_ShowDeleted_Host',
|
|
771
|
+
`<input class="prsp-filters-showdeleted-checkbox" type="checkbox" ${tmpChecked} onchange="_Pict.views['PRSP-Filters'].toggleShowDeletedFilter('${pRecordSet}', '${pViewContext}', this.checked)"> Show deleted`);
|
|
772
|
+
}
|
|
773
|
+
|
|
722
774
|
applyQuickFilterText(pRecordSet, pViewContext, pField, pClauseKey, pValue)
|
|
723
775
|
{
|
|
724
776
|
this.bumpRenderEpoch();
|
|
@@ -1162,7 +1214,8 @@ class ViewRecordSetSUBSETFilters extends libPictView
|
|
|
1162
1214
|
if (tmpFilterRecordSet)
|
|
1163
1215
|
{
|
|
1164
1216
|
this._paintFilterControls(tmpFilterRecordSet);
|
|
1165
|
-
|
|
1217
|
+
this._renderQuickFilters(tmpFilterRecordSet, tmpFilterViewContext);
|
|
1218
|
+
this._renderShowDeletedControl(tmpFilterRecordSet, tmpFilterViewContext);
|
|
1166
1219
|
// (Re)render the experiences dropdown only when its container is empty — i.e. on
|
|
1167
1220
|
// a fresh filter render — not on every sub-render (add-filter dropdown, etc.).
|
|
1168
1221
|
const tmpExpContainer = document.getElementById('FilterPersistenceView-Container');
|