pict-section-recordset 1.0.43 → 1.0.44
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/example_applications/simple_entity/Simple-RecordSet-Application.js +198 -3
- package/example_applications/simple_entity/html/index.html +6 -0
- package/package.json +2 -2
- package/source/providers/Filter-Data-Provider.js +371 -0
- package/source/providers/RecordSet-Link-Manager.js +9 -1
- package/source/providers/RecordSet-RecordProvider-Base.js +1 -1
- package/source/providers/RecordSet-RecordProvider-MeadowEndpoints.js +2 -0
- package/source/services/RecordsSet-MetaController.js +39 -0
- package/source/templates/Pict-Template-FilterInstanceViews.js +11 -7
- package/source/views/RecordSet-Filters.js +78 -45
- package/source/views/filters/RecordSet-Filter-Base.js +19 -1
- package/source/views/filters/RecordSet-Filter-ExternalJoinSelectedValue.js +270 -0
- package/source/views/filters/RecordSet-Filter-ExternalJoinSelectedValueList.js +277 -0
- package/source/views/filters/RecordSet-Filter-InternalJoinSelectedValue.js +270 -0
- package/source/views/filters/RecordSet-Filter-InternalJoinSelectedValueList.js +277 -0
- package/source/views/filters/index.js +4 -0
- package/types/providers/Filter-Data-Provider.d.ts +120 -0
- package/types/providers/Filter-Data-Provider.d.ts.map +1 -0
- package/types/providers/RecordSet-Link-Manager.d.ts +10 -5
- package/types/providers/RecordSet-Link-Manager.d.ts.map +1 -1
- package/types/providers/RecordSet-RecordProvider-MeadowEndpoints.d.ts.map +1 -1
- package/types/services/RecordsSet-MetaController.d.ts +36 -7
- package/types/services/RecordsSet-MetaController.d.ts.map +1 -1
- package/types/templates/Pict-Template-FilterInstanceViews.d.ts +1 -1
- package/types/templates/Pict-Template-FilterInstanceViews.d.ts.map +1 -1
- package/types/views/RecordSet-Filters.d.ts +10 -0
- package/types/views/RecordSet-Filters.d.ts.map +1 -1
- package/types/views/filters/RecordSet-Filter-Base.d.ts +10 -0
- package/types/views/filters/RecordSet-Filter-Base.d.ts.map +1 -1
- package/types/views/filters/RecordSet-Filter-ExternalJoinSelectedValue.d.ts +24 -0
- package/types/views/filters/RecordSet-Filter-ExternalJoinSelectedValue.d.ts.map +1 -0
- package/types/views/filters/RecordSet-Filter-ExternalJoinSelectedValueList.d.ts +24 -0
- package/types/views/filters/RecordSet-Filter-ExternalJoinSelectedValueList.d.ts.map +1 -0
- package/types/views/filters/RecordSet-Filter-InternalJoinSelectedValue.d.ts +24 -0
- package/types/views/filters/RecordSet-Filter-InternalJoinSelectedValue.d.ts.map +1 -0
- package/types/views/filters/RecordSet-Filter-InternalJoinSelectedValueList.d.ts +24 -0
- package/types/views/filters/RecordSet-Filter-InternalJoinSelectedValueList.d.ts.map +1 -0
- package/types/views/filters/index.d.ts +4 -0
|
@@ -1,7 +1,165 @@
|
|
|
1
1
|
const libPictRecordSet = require('../../source/Pict-Section-RecordSet.js');
|
|
2
2
|
|
|
3
|
+
const BaseFilterView = require('../../source/views/filters/RecordSet-Filter-ExternalJoinSelectedValueList.js');
|
|
4
|
+
|
|
5
|
+
const CustomViewConfig =
|
|
6
|
+
{
|
|
7
|
+
ViewIdentifier: 'PRSP-FilterType-ExternalJoinSelectedValueList-MyCoolView',
|
|
8
|
+
|
|
9
|
+
Templates:
|
|
10
|
+
[
|
|
11
|
+
{
|
|
12
|
+
Hash: 'PRSP-Filter-ExternalJoin-SelectedValueList-Template-Custom',
|
|
13
|
+
Template: /*html*/`
|
|
14
|
+
<!-- DefaultPackage pict view template: [PRSP-Filter-ExternalJoin-SelectedValueList-Template] -->
|
|
15
|
+
<table>
|
|
16
|
+
<tbody>
|
|
17
|
+
<td valign="top">{~T:PRSP-Filter-ExternalJoin-SelectedValueList-SearchResults-Custom~}</td>
|
|
18
|
+
<td>
|
|
19
|
+
<button onclick="_Pict.views['{~D:Context[0].Hash~}'].handleAdd(event, '{~D:Record.ClauseAddress~}', '{~D:Record.Hash~}')">--></button>
|
|
20
|
+
<button onclick="_Pict.views['{~D:Context[0].Hash~}'].handleRemove(event, '{~D:Record.ClauseAddress~}', '{~D:Record.Hash~}')"><--</button>
|
|
21
|
+
</td>
|
|
22
|
+
<td valign="top">{~T:PRSP-Filter-ExternalJoin-SelectedValueList-SelectedValues-Custom~}</td>
|
|
23
|
+
</tbody>
|
|
24
|
+
</table>
|
|
25
|
+
<!-- DefaultPackage end view template: [PRSP-Filter-ExternalJoin-SelectedValueList-Template] -->
|
|
26
|
+
`
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
Hash: 'PRSP-Filter-ExternalJoin-SelectedValueList-SearchResults-Custom',
|
|
30
|
+
Template: /*html*/`
|
|
31
|
+
<!-- DefaultPackage pict view template: [PRSP-Filter-ExternalJoin-SearchResults] -->
|
|
32
|
+
<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;">
|
|
33
|
+
<input id="PRSP_Filter_{~D:Record.Hash~}_Search_Value" type="text" placeholder="Search..." value="{~D:Record.SearchInputValue~}" />
|
|
34
|
+
<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>
|
|
35
|
+
</form>
|
|
36
|
+
<label>{~D:Record.Label~}</label>
|
|
37
|
+
<select id="PRSP_Filter_{~D:Record.Hash~}_ResultsList" size="25">
|
|
38
|
+
{~TSWP:PRSP-Filter-ExternalJoin-SelectedValueList-SearchResults-Entry-Custom:Record.SearchResults:Record~}
|
|
39
|
+
</select>
|
|
40
|
+
<!-- DefaultPackage end view template: [PRSP-Filter-ExternalJoin-SearchResults] -->
|
|
41
|
+
`
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
Hash: 'PRSP-Filter-ExternalJoin-SelectedValueList-SelectedValues-Custom',
|
|
45
|
+
Template: /*html*/`
|
|
46
|
+
<!-- DefaultPackage view template: [PRSP-Filter-ExternalJoin-SelectedValueList-SelectedValues] -->
|
|
47
|
+
<label>Selection</label>
|
|
48
|
+
<select id="PRSP_Filter_{~D:Record.Hash~}_SelectedValuesSelect" size="25">
|
|
49
|
+
{~TSWP:PRSP-Filter-ExternalJoin-SelectedValueList-SelectedValues-Entry-Custom:Record.SelectedValues:Record~}
|
|
50
|
+
</select>
|
|
51
|
+
<!-- DefaultPackage end view template: [PRSP-Filter-ExternalJoin-SelectedValueList-SelectedValues] -->
|
|
52
|
+
`
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
Hash: 'PRSP-Filter-ExternalJoin-SelectedValueList-SearchResults-Entry-Custom',
|
|
56
|
+
Template: /*html*/`
|
|
57
|
+
<!-- DefaultPackage pict view template: [PRSP-Filter-ExternalJoin-SelectedValueList-Template] -->
|
|
58
|
+
<option value="{~DVBK:Record.Data:Record.Payload.ExternalFilterTableLookupColumn~}">{~TFA:Record.Payload.ExternalRecordDisplayTemplate:Record~}</option>
|
|
59
|
+
<!-- DefaultPackage end view template: [PRSP-Filter-ExternalJoin-SelectedValueList-Template] -->
|
|
60
|
+
`
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
Hash: 'PRSP-Filter-ExternalJoin-SelectedValueList-SelectedValues-Entry-Custom',
|
|
64
|
+
Template: /*html*/`
|
|
65
|
+
<!-- DefaultPackage pict view template: [PRSP-Filter-ExternalJoin-SelectedValueList-SelectedValues-Entry] -->
|
|
66
|
+
<option value="{~DVBK:Record.Data:Record.Payload.ExternalFilterTableLookupColumn~}">{~TFA:Record.Payload.ExternalRecordDisplayTemplate:Record~}</option>
|
|
67
|
+
<!-- DefaultPackage end view template: [PRSP-Filter-ExternalJoin-SelectedValueList-SelectedValues-Entry] -->
|
|
68
|
+
`
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
class CustomFilterView extends BaseFilterView
|
|
74
|
+
{
|
|
75
|
+
getFilterFormTemplate()
|
|
76
|
+
{
|
|
77
|
+
return 'PRSP-Filter-ExternalJoin-SelectedValueList-Template-Custom';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
handleAdd(pEvent, pClauseInformaryAddress, pClauseHash)
|
|
81
|
+
{
|
|
82
|
+
pEvent.preventDefault();
|
|
83
|
+
const tmpClause = this.getInformaryScopedValue(pClauseInformaryAddress);
|
|
84
|
+
if (!tmpClause)
|
|
85
|
+
{
|
|
86
|
+
this.pict.log.error(`[Filter-ExternalJoinSelectedValueList] No clause found for address: ${pClauseInformaryAddress}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const tmpRecordLookupValue = this.pict.ContentAssignment.readContent(`#PRSP_Filter_${tmpClause.Hash}_ResultsList`);
|
|
90
|
+
const tmpRecordLookupColumn = tmpClause.ExternalFilterTableLookupColumn || `ID${tmpClause.ExternalFilterByTable}`;
|
|
91
|
+
const tmpRecordToAdd = tmpClause.SearchResults.find((r) => r[tmpRecordLookupColumn] == tmpRecordLookupValue);
|
|
92
|
+
if (!tmpRecordToAdd)
|
|
93
|
+
{
|
|
94
|
+
this.pict.log.error(`[Filter-ExternalJoinSelectedValueList] No record found for value: ${tmpRecordLookupValue} in search results.`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!tmpClause.SelectedValues)
|
|
98
|
+
{
|
|
99
|
+
tmpClause.SelectedValues = [];
|
|
100
|
+
}
|
|
101
|
+
if (tmpClause.SelectedValues.some((pSV) => pSV[tmpRecordLookupColumn] == tmpRecordLookupValue))
|
|
102
|
+
{
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const tmpValue = tmpRecordToAdd[tmpClause.ExternalFilterByTableConnectionColumn];
|
|
106
|
+
if (tmpValue == null)
|
|
107
|
+
{
|
|
108
|
+
this.pict.log.error(`[Filter-ExternalJoinSelectedValueList] No value found for column: ${tmpClause.ExternalFilterByTableConnectionColumn} in record: ${JSON.stringify(tmpRecordToAdd)}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
tmpClause.SelectedValues.push(tmpRecordToAdd);
|
|
112
|
+
if (!tmpClause.Values)
|
|
113
|
+
{
|
|
114
|
+
tmpClause.Values = [];
|
|
115
|
+
}
|
|
116
|
+
if (!tmpClause.Values.some((pV) => pV == tmpValue))
|
|
117
|
+
{
|
|
118
|
+
tmpClause.Values.push(tmpValue);
|
|
119
|
+
}
|
|
120
|
+
const tmpRecord = Object.assign({ ClauseAddress: pClauseInformaryAddress }, tmpClause);
|
|
121
|
+
this.prepareRecord(tmpRecord);
|
|
122
|
+
const tmpDestinationAddress = `#PRSP_Filter_Container_${pClauseHash}`;
|
|
123
|
+
this.render(null, tmpDestinationAddress, tmpRecord);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
handleRemove(pEvent, pClauseInformaryAddress, pClauseHash)
|
|
127
|
+
{
|
|
128
|
+
pEvent.preventDefault();
|
|
129
|
+
const tmpClause = this.getInformaryScopedValue(pClauseInformaryAddress);
|
|
130
|
+
if (!tmpClause)
|
|
131
|
+
{
|
|
132
|
+
this.pict.log.error(`[Filter-ExternalJoinSelectedValueList] No clause found for address: ${pClauseInformaryAddress}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const tmpRecordLookupValue = this.pict.ContentAssignment.readContent(`#PRSP_Filter_${tmpClause.Hash}_SelectedValuesSelect`);
|
|
136
|
+
const tmpRecordLookupColumn = tmpClause.ExternalFilterTableLookupColumn || `ID${tmpClause.ExternalFilterByTable}`;
|
|
137
|
+
const tmpRecordIndexToRemove = tmpClause.SelectedValues.findIndex((r) => r[tmpRecordLookupColumn] == tmpRecordLookupValue);
|
|
138
|
+
if (tmpRecordIndexToRemove < 0)
|
|
139
|
+
{
|
|
140
|
+
this.pict.log.error(`[Filter-ExternalJoinSelectedValueList] No record found to remove for value: ${tmpRecordLookupValue}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const tmpRecordToRemove = tmpClause.SelectedValues.splice(tmpRecordIndexToRemove, 1)[0];
|
|
144
|
+
const tmpValue = tmpRecordToRemove[tmpClause.ExternalFilterByTableConnectionColumn];
|
|
145
|
+
tmpClause.Values = tmpClause.Values.filter((pV) => pV !== tmpValue);
|
|
146
|
+
const tmpRecord = Object.assign({ ClauseAddress: pClauseInformaryAddress }, tmpClause);
|
|
147
|
+
this.prepareRecord(tmpRecord);
|
|
148
|
+
const tmpDestinationAddress = `#PRSP_Filter_Container_${pClauseHash}`;
|
|
149
|
+
this.render(null, tmpDestinationAddress, tmpRecord);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
CustomFilterView.default_configuration = Object.assign({}, BaseFilterView.default_configuration, CustomViewConfig);
|
|
154
|
+
|
|
3
155
|
class SimpleApplication extends libPictRecordSet.PictRecordSetApplication
|
|
4
156
|
{
|
|
157
|
+
onInitialize()
|
|
158
|
+
{
|
|
159
|
+
this.pict.addView('PRSP-FilterType-ExternalJoinSelectedValueList-MyCoolView', {}, CustomFilterView);
|
|
160
|
+
|
|
161
|
+
return super.onInitialize();
|
|
162
|
+
}
|
|
5
163
|
}
|
|
6
164
|
|
|
7
165
|
module.exports = SimpleApplication;
|
|
@@ -182,7 +340,7 @@ module.exports.default_configuration.pict_configuration = (
|
|
|
182
340
|
{
|
|
183
341
|
"ExternalJoinBookByAuthor":
|
|
184
342
|
{
|
|
185
|
-
"
|
|
343
|
+
"Label": "Author's Name",
|
|
186
344
|
"Type": "ExternalJoinStringMatch",
|
|
187
345
|
"ExternalFilterByColumns": [ "Name" ],
|
|
188
346
|
|
|
@@ -194,6 +352,39 @@ module.exports.default_configuration.pict_configuration = (
|
|
|
194
352
|
|
|
195
353
|
"ExternalFilterByTable": "Author",
|
|
196
354
|
"ExternalFilterByTableConnectionColumn": "IDAuthor"
|
|
355
|
+
},
|
|
356
|
+
"ExternalJoinBookBySelectedAuthors":
|
|
357
|
+
{
|
|
358
|
+
"Label": "Authors",
|
|
359
|
+
"Type": "ExternalJoinSelectedValueList",
|
|
360
|
+
"ExternalFilterByColumns": [ "Name", "GUIDAuthor" ],
|
|
361
|
+
|
|
362
|
+
"MaximumSelectedExternalRecords": 5,
|
|
363
|
+
"ExternalRecordDisplayTemplate": "{~D:Record.Data.Name~}",
|
|
364
|
+
|
|
365
|
+
"CoreConnectionColumn": "IDBook",
|
|
366
|
+
|
|
367
|
+
"JoinTable": "BookAuthorJoin",
|
|
368
|
+
"JoinTableExternalConnectionColumn": "IDAuthor",
|
|
369
|
+
"JoinTableCoreConnectionColumn": "IDBook",
|
|
370
|
+
|
|
371
|
+
"ExternalFilterByTable": "Author",
|
|
372
|
+
"ExternalFilterByTableConnectionColumn": "IDAuthor",
|
|
373
|
+
"CustomFilterViewHash": "MyCoolView"
|
|
374
|
+
},
|
|
375
|
+
"InternalJoinBySelectedJoinIDs":
|
|
376
|
+
{
|
|
377
|
+
"Label": "Join IDs (internal test)",
|
|
378
|
+
"Type": "InternalJoinSelectedValue",
|
|
379
|
+
"ExternalFilterByColumns": [ "IDBookAuthorJoin" ],
|
|
380
|
+
|
|
381
|
+
"ExternalRecordDisplayTemplate": "{~D:Record.Data.IDBookAuthorJoin~} [{~D:Record.Data.IDBook~} <-> {~D:Record.Data.IDAuthor~}]",
|
|
382
|
+
|
|
383
|
+
"CoreConnectionColumn": "IDBook",
|
|
384
|
+
|
|
385
|
+
"RemoteTable": "BookAuthorJoin",
|
|
386
|
+
"JoinExternalConnectionColumn": "IDBook",
|
|
387
|
+
"JoinInternalConnectionColumn": "IDBook"
|
|
197
388
|
}
|
|
198
389
|
},
|
|
199
390
|
"FilterCriteria":
|
|
@@ -201,7 +392,11 @@ module.exports.default_configuration.pict_configuration = (
|
|
|
201
392
|
"FilterRecordsetByBookAndCreateDate":
|
|
202
393
|
[
|
|
203
394
|
{
|
|
204
|
-
"FilterDefinitionHash": "
|
|
395
|
+
"FilterDefinitionHash": "ExternalJoinBookBySelectedAuthors",
|
|
396
|
+
"FilterByColumn": "IDBook"
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
"FilterDefinitionHash": "InternalJoinBySelectedJoinIDs",
|
|
205
400
|
"FilterByColumn": "IDBook"
|
|
206
401
|
},
|
|
207
402
|
{
|
|
@@ -236,7 +431,7 @@ module.exports.default_configuration.pict_configuration = (
|
|
|
236
431
|
{
|
|
237
432
|
"Title":
|
|
238
433
|
[
|
|
239
|
-
{ "FilterKey": "Title", "ClauseKey": "TitleMatch", "
|
|
434
|
+
{ "FilterKey": "Title", "ClauseKey": "TitleMatch", "Label": "Book Title Custom Filter", "Type": "StringMatch", "FilterByColumn": "Title", "ExactMatch": true }
|
|
240
435
|
]
|
|
241
436
|
},
|
|
242
437
|
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<title>Simple.</title>
|
|
5
5
|
<style id="PICT-CSS"></style>
|
|
6
|
+
<style>
|
|
7
|
+
.is-hidden
|
|
8
|
+
{
|
|
9
|
+
display: none;
|
|
10
|
+
}
|
|
11
|
+
</style>
|
|
6
12
|
<script src="./pict.js" type="text/javascript"></script>
|
|
7
13
|
<script type="text/javascript">Pict.safeOnDocumentReady(() => { Pict.safeLoadPictApplication(SimpleApplication, 1)});</script>
|
|
8
14
|
</head>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pict-section-recordset",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.44",
|
|
4
4
|
"description": "Pict dynamic record set management views",
|
|
5
5
|
"main": "source/Pict-Section-RecordSet.js",
|
|
6
6
|
"directories": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"browser-env": "^3.3.0",
|
|
34
34
|
"eslint": "^9.28.0",
|
|
35
35
|
"jquery": "^3.7.1",
|
|
36
|
-
"pict": "^1.0.
|
|
36
|
+
"pict": "^1.0.299",
|
|
37
37
|
"pict-application": "^1.0.27",
|
|
38
38
|
"pict-service-commandlineutility": "^1.0.15",
|
|
39
39
|
"quackage": "^1.0.42",
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
const libPictProvider = require('pict-provider');
|
|
2
|
+
|
|
3
|
+
const _DEFAULT_PROVIDER_CONFIGURATION =
|
|
4
|
+
{
|
|
5
|
+
ProviderIdentifier: 'FilterDataProvider',
|
|
6
|
+
|
|
7
|
+
AutoInitialize: true,
|
|
8
|
+
AutoInitializeOrdinal: 0,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class FilterDataProvider extends libPictProvider
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* @param {import('pict')} pFable - The Fable instance
|
|
15
|
+
* @param {Record<string, any>} [pOptions] - The options for the provider
|
|
16
|
+
* @param {string} [pServiceHash] - The service hash for the provider
|
|
17
|
+
*/
|
|
18
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
19
|
+
{
|
|
20
|
+
let tmpOptions = Object.assign({}, _DEFAULT_PROVIDER_CONFIGURATION, pOptions);
|
|
21
|
+
super(pFable, tmpOptions, pServiceHash);
|
|
22
|
+
|
|
23
|
+
// This allows unit tests to run
|
|
24
|
+
this.storageProvider = this;
|
|
25
|
+
this.keyCache = {};
|
|
26
|
+
if ((typeof(window) === 'object') && (typeof(window.localStorage) === 'object'))
|
|
27
|
+
{
|
|
28
|
+
this.storageProvider = window.localStorage;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.filtersMap = { };
|
|
32
|
+
this.lastFilterExperienceHashMap = { };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onBeforeInitialize()
|
|
36
|
+
{
|
|
37
|
+
this.loadFilters();
|
|
38
|
+
|
|
39
|
+
return super.onBeforeInitialize();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @return {void}
|
|
44
|
+
*/
|
|
45
|
+
loadFilters()
|
|
46
|
+
{
|
|
47
|
+
// load all known recordsets
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* List all available Filters (from the Filter Meta data)
|
|
52
|
+
*
|
|
53
|
+
* @param {string} pRecordSet - the record set to list the filters for
|
|
54
|
+
*
|
|
55
|
+
* @return Array<Record<string, any>> - a list of Filters as Index/FilterExperienceHash entries
|
|
56
|
+
*/
|
|
57
|
+
listFilters(pRecordSet)
|
|
58
|
+
{
|
|
59
|
+
this.loadFilterMeta(pRecordSet);
|
|
60
|
+
const filtersList = this.filtersMap[pRecordSet] || [];
|
|
61
|
+
const tmpFilterList = filtersList.map((pValue, pIndex) => { return { Index: pIndex, RecordSet: pRecordSet, FilterExperienceHash: pValue }; });
|
|
62
|
+
return tmpFilterList;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string} pRecordSet - the record set to get the active filter experience for
|
|
67
|
+
*
|
|
68
|
+
* @return {Record<string, any>} - the active filter experience for the given record set
|
|
69
|
+
*/
|
|
70
|
+
getActiveFilterExperience(pRecordSet)
|
|
71
|
+
{
|
|
72
|
+
return this.pict.Bundle._ActiveFilterState?.[pRecordSet];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if a particular scope is in use.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} pRecordSet - the record set
|
|
79
|
+
* @param {string} pFilterExperienceHash - the manyfest scope to check the existence of
|
|
80
|
+
*
|
|
81
|
+
* @return {boolean}
|
|
82
|
+
*/
|
|
83
|
+
checkFilterExists(pRecordSet, pFilterExperienceHash)
|
|
84
|
+
{
|
|
85
|
+
const filterExperience = this.getActiveFilterExperience(pRecordSet);
|
|
86
|
+
if (filterExperience && filterExperience.FilterExperienceHash === pFilterExperienceHash)
|
|
87
|
+
{
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
// Make sure other tabs didn't do something funny.
|
|
91
|
+
// Also. This means users can do FUNNY BUSINESS and mess with the state if they have a
|
|
92
|
+
// crapton of tabs open and delete a manyfest in one tab and later this check happens.
|
|
93
|
+
// Will not result in data loss but will result in flaky behavior.
|
|
94
|
+
this.loadFilterMeta(pRecordSet, false);
|
|
95
|
+
const filtersList = this.filtersMap[pRecordSet] || [];
|
|
96
|
+
return filtersList.indexOf(pFilterExperienceHash) >= 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a key in the LocalStorage keyspace for a filter experience for a given record set.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} pRecordSet - The record set to resolve a key for
|
|
103
|
+
* @param {string} pFilterExperienceHash - The scope to resolve a key for
|
|
104
|
+
*
|
|
105
|
+
* @return {string} A string that points to the record.
|
|
106
|
+
*/
|
|
107
|
+
getFilterStorageKey(pRecordSet, pFilterExperienceHash)
|
|
108
|
+
{
|
|
109
|
+
this.loadFilterMeta(pRecordSet, false);
|
|
110
|
+
// Default to the loaded manyfest if nothing is passed in.
|
|
111
|
+
let tmpFilterExperienceHash = (typeof(pFilterExperienceHash) === 'string') ? pFilterExperienceHash : this.pict.AppData?.FilterRecord?.FilterExperienceHash ?? 'RECENT';
|
|
112
|
+
return `${pRecordSet}_Filter_${tmpFilterExperienceHash}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Save the application metadata (list of Filters, last loaded FilterExperienceHash, etc.)
|
|
117
|
+
*
|
|
118
|
+
* @param {string} pRecordSet - The record set to save the filter for; TODO: should this have a default?
|
|
119
|
+
* @param {boolean} [pRender=false] - Whether or not to also render the list of Filters in the UI automatically
|
|
120
|
+
*/
|
|
121
|
+
saveFilterMeta(pRecordSet, pRender = false)
|
|
122
|
+
{
|
|
123
|
+
const filtersList = this.filtersMap[pRecordSet] || [];
|
|
124
|
+
|
|
125
|
+
// TODO: BUG: Gotta have a more complex merge happen here for multiple tabs
|
|
126
|
+
this.storageProvider.setItem(`Filter_Meta_${pRecordSet}`, JSON.stringify({ LastFilterExperienceHash: this.lastFilterExperienceHashMap[pRecordSet] || 'RECENT', FilterList: filtersList }));
|
|
127
|
+
|
|
128
|
+
if (pRender && this.pict.views.FilterPersistenceView)
|
|
129
|
+
{
|
|
130
|
+
this.pict.views.FilterPersistenceView.render()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Save the application metadata (list of Filters, last loaded FilterExperienceHash, etc.)
|
|
138
|
+
*
|
|
139
|
+
* @param {string} pRecordSet - The record set to save the filter for; TODO: should this have a default?
|
|
140
|
+
* @param {boolean} [pRender=false] - Whether or not to also render the list of Filters in the UI automatically
|
|
141
|
+
*
|
|
142
|
+
* @return {Array<object>} The list of available Filters.
|
|
143
|
+
*/
|
|
144
|
+
loadFilterMeta(pRecordSet, pRender = false)
|
|
145
|
+
{
|
|
146
|
+
// We get this every time in case the user has multiple tabs open
|
|
147
|
+
let tmpFilterMetaJSON = this.storageProvider.getItem(`Filter_Meta_${pRecordSet}`);
|
|
148
|
+
if (!tmpFilterMetaJSON)
|
|
149
|
+
{
|
|
150
|
+
tmpFilterMetaJSON = '{}';
|
|
151
|
+
}
|
|
152
|
+
let tmpFilterMeta = JSON.parse(tmpFilterMetaJSON)
|
|
153
|
+
|
|
154
|
+
let filtersList = tmpFilterMeta.FilterList;
|
|
155
|
+
this.filtersMap[pRecordSet] = filtersList;
|
|
156
|
+
this.lastFilterExperienceHashMap[pRecordSet] = tmpFilterMeta.LastFilterExperienceHash;
|
|
157
|
+
|
|
158
|
+
if (!Array.isArray(filtersList))
|
|
159
|
+
{
|
|
160
|
+
this.filtersMap[pRecordSet] = [];
|
|
161
|
+
//this.lastFilterExperienceHashMap[pRecordSet] = 'LATEST';
|
|
162
|
+
this.saveFilterMeta(pRecordSet)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (pRender && this.pict.views.FilterPersistenceView)
|
|
166
|
+
{
|
|
167
|
+
this.pict.views.FilterPersistenceView.render()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return filtersList;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {string} pRecordSet - The record set to add the filter experience hash to
|
|
175
|
+
* @param {string} pFilterExperienceHash - The filter experience hash to add
|
|
176
|
+
* @param {boolean} [pRender=true] - Whether or not to also render the list of Filters in the UI automatically
|
|
177
|
+
*
|
|
178
|
+
* @return {boolean} True if the filter experience hash was added, false if it already exists.
|
|
179
|
+
*/
|
|
180
|
+
addFilterExperienceHashToFilterList(pRecordSet, pFilterExperienceHash, pRender)
|
|
181
|
+
{
|
|
182
|
+
let tmpRender = (typeof(pRender) === 'undefined') ? true : pRender;
|
|
183
|
+
|
|
184
|
+
this.loadFilterMeta(pRecordSet);
|
|
185
|
+
|
|
186
|
+
const tmpFilterList = this.filtersMap[pRecordSet] || [];
|
|
187
|
+
|
|
188
|
+
let tmpFilterFilterExperienceHashIndex = tmpFilterList.indexOf(pFilterExperienceHash);
|
|
189
|
+
|
|
190
|
+
if (tmpFilterFilterExperienceHashIndex >= 0)
|
|
191
|
+
{
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
const filtersList = this.filtersMap[pRecordSet] || [];
|
|
195
|
+
filtersList.push(pFilterExperienceHash);
|
|
196
|
+
this.filtersMap[pRecordSet] = filtersList;
|
|
197
|
+
|
|
198
|
+
this.saveFilterMeta(pRecordSet);
|
|
199
|
+
|
|
200
|
+
if (tmpRender && this.pict.views.FilterPersistenceView)
|
|
201
|
+
{
|
|
202
|
+
this.pict.views.FilterPersistenceView.render()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @param {string} pRecordSet - The record set to remove the filter experience hash from
|
|
210
|
+
* @param {string} pFilterExperienceHash - The filter experience hash to remove
|
|
211
|
+
* @param {boolean} [pRender=true] - Whether or not to also render the list of Filters in the UI automatically
|
|
212
|
+
*/
|
|
213
|
+
removeFilterExperienceHashFromFilterList(pRecordSet, pFilterExperienceHash, pRender)
|
|
214
|
+
{
|
|
215
|
+
let tmpRender = (typeof(pRender) === 'undefined') ? true : pRender;
|
|
216
|
+
|
|
217
|
+
let tmpFilterList = this.loadFilterMeta(pRecordSet);
|
|
218
|
+
|
|
219
|
+
let tmpFilterFilterExperienceHashIndex = tmpFilterList.indexOf(pFilterExperienceHash);
|
|
220
|
+
|
|
221
|
+
if (tmpFilterFilterExperienceHashIndex >= 0)
|
|
222
|
+
{
|
|
223
|
+
// Could use array splice but meh
|
|
224
|
+
let tmpNewFilterList = [];
|
|
225
|
+
|
|
226
|
+
for (let i = 0; i < tmpFilterList.length; i++)
|
|
227
|
+
{
|
|
228
|
+
if (tmpFilterList[i] != pFilterExperienceHash)
|
|
229
|
+
{
|
|
230
|
+
tmpNewFilterList.push(tmpFilterList[i]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.filtersMap[pRecordSet] = tmpNewFilterList;
|
|
235
|
+
|
|
236
|
+
this.saveFilterMeta(pRecordSet, true);
|
|
237
|
+
|
|
238
|
+
if (tmpRender && this.pict.views.FilterPersistenceView)
|
|
239
|
+
{
|
|
240
|
+
this.pict.views.FilterPersistenceView.render()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @param {string} pRecordSet - The record set to create a new filter for
|
|
251
|
+
* @param {string} [pFilterExperienceHash] - The filter experience hash to create a new filter for; if not provided, a new one will be generated
|
|
252
|
+
*/
|
|
253
|
+
newFilter(pRecordSet, pFilterExperienceHash)
|
|
254
|
+
{
|
|
255
|
+
let tmpFilterExperienceHash = ((typeof(pFilterExperienceHash) === 'string') && (pFilterExperienceHash.length > 0)) ? pFilterExperienceHash : false;
|
|
256
|
+
|
|
257
|
+
if (!tmpFilterExperienceHash)
|
|
258
|
+
{
|
|
259
|
+
// Autogenerate a scope
|
|
260
|
+
const filtersList = this.filtersMap[pRecordSet] || [];
|
|
261
|
+
let tmpProspectiveIndex = filtersList.length;
|
|
262
|
+
|
|
263
|
+
// If a user has more than 10,000 manifests we need to talk. In person.
|
|
264
|
+
for (let i = 0; i < 10000; i++)
|
|
265
|
+
{
|
|
266
|
+
let tmpFilterFilterExperienceHash = `New Manifest ${tmpProspectiveIndex}`;
|
|
267
|
+
|
|
268
|
+
if (!this.checkFilterExists(pRecordSet, tmpFilterFilterExperienceHash))
|
|
269
|
+
{
|
|
270
|
+
tmpFilterExperienceHash = tmpFilterFilterExperienceHash;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else
|
|
276
|
+
{
|
|
277
|
+
// Check to ensure the manyfest doesn't already exist.
|
|
278
|
+
if (this.checkFilterExists(pRecordSet, tmpFilterExperienceHash))
|
|
279
|
+
{
|
|
280
|
+
this.log.warn(`Filter ${tmpFilterExperienceHash} already exists but it was explicitly requested by the user. Loading insted.`);
|
|
281
|
+
return this.loadFilter(pRecordSet, tmpFilterExperienceHash);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// As far as I can tell this only happens if the user has more than 10,000 manifests
|
|
286
|
+
if (!tmpFilterExperienceHash)
|
|
287
|
+
{
|
|
288
|
+
this.log.warn(`You have won the lottery. Seriously. Call us to learn more! Please email steven@velozo.com for more details.`);
|
|
289
|
+
tmpFilterExperienceHash = 'LotteryWinner';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Now create the new manyfest
|
|
293
|
+
let tmpNewManifest = JSON.parse(JSON.stringify(this.options.DefaultManifest));
|
|
294
|
+
tmpNewManifest.FilterExperienceHash = tmpFilterExperienceHash;
|
|
295
|
+
|
|
296
|
+
// Now save it.
|
|
297
|
+
this.storageProvider.setItem(this.getFilterStorageKey(pRecordSet, tmpFilterExperienceHash), JSON.stringify(tmpNewManifest));
|
|
298
|
+
this.addFilterExperienceHashToFilterList(pRecordSet, tmpFilterExperienceHash, true);
|
|
299
|
+
|
|
300
|
+
// TODO: Do we load it? Maaaaaaaybe. Figure out the "autosave before load" workflow.
|
|
301
|
+
|
|
302
|
+
this.loadFilter(pRecordSet, tmpFilterExperienceHash);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @param {string} pRecordSet - The record set to save the filter for; TODO: should this have a default?
|
|
307
|
+
*/
|
|
308
|
+
saveFilter(pRecordSet)
|
|
309
|
+
{
|
|
310
|
+
let tmpFilterFilterExperienceHash = this.pict.AppData.FilterRecord.FilterExperienceHash;
|
|
311
|
+
// TODO: Should this be a .... merge? Yikes. Multiple tabs is bonkers.
|
|
312
|
+
this.storageProvider.setItem(this.getFilterStorageKey(pRecordSet, tmpFilterFilterExperienceHash), JSON.stringify(this.pict.AppData.FilterRecord));
|
|
313
|
+
this.addFilterExperienceHashToFilterList(pRecordSet, tmpFilterFilterExperienceHash);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @param {string} pRecordSet - The record set to load the filter for
|
|
318
|
+
* @param {string} pFilterFilterExperienceHash - The filter experience hash to load; if not provided, the last used one will be loaded
|
|
319
|
+
*/
|
|
320
|
+
loadFilter(pRecordSet, pFilterFilterExperienceHash)
|
|
321
|
+
{
|
|
322
|
+
let tmpFilterJSON = this.storageProvider.getItem(this.getFilterStorageKey(pRecordSet, pFilterFilterExperienceHash));
|
|
323
|
+
if (tmpFilterJSON)
|
|
324
|
+
{
|
|
325
|
+
this.pict.AppData.FilterRecord = JSON.parse(tmpFilterJSON);
|
|
326
|
+
}
|
|
327
|
+
this.pict.providers.FilterRouter.postPersistNavigate();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* @param {string} pKey - The key to get from the cache
|
|
332
|
+
*
|
|
333
|
+
* @return {any} - The value associated with the key, or false if not found
|
|
334
|
+
*/
|
|
335
|
+
getItem(pKey)
|
|
336
|
+
{
|
|
337
|
+
if (pKey in this.keyCache)
|
|
338
|
+
{
|
|
339
|
+
return this.keyCache[pKey];
|
|
340
|
+
}
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @param {string} pKey - The key to set in the cache
|
|
346
|
+
* @param {any} pValue - The value to associate with the key
|
|
347
|
+
*/
|
|
348
|
+
setItem(pKey, pValue)
|
|
349
|
+
{
|
|
350
|
+
this.keyCache[pKey] = pValue;
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @param {string} pKey - The key to remove from the cache
|
|
356
|
+
*
|
|
357
|
+
* @return {boolean} - True if the item was removed, false if it was not found
|
|
358
|
+
*/
|
|
359
|
+
removeItem(pKey)
|
|
360
|
+
{
|
|
361
|
+
if (pKey in this.keyCache)
|
|
362
|
+
{
|
|
363
|
+
delete this.keyCache[pKey];
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
module.exports = FilterDataProvider;
|
|
371
|
+
module.exports.default_configuration = _DEFAULT_PROVIDER_CONFIGURATION;
|
|
@@ -31,7 +31,15 @@ class PictRecordSetLinkManager extends libPictProvider
|
|
|
31
31
|
this.linkTemplates = [];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
/**
|
|
35
|
+
* TODO: Add the ability to add routes that are scoped to particular entity types
|
|
36
|
+
*
|
|
37
|
+
* @param {string} pNameTemplate - The name template for the record link.
|
|
38
|
+
* @param {string} pURLTemplate - The URL template for the record link.
|
|
39
|
+
* @param {boolean} pDefault - Whether this is a default link template.
|
|
40
|
+
*
|
|
41
|
+
* @return {Record<string, any>} - The link template object that was added.
|
|
42
|
+
*/
|
|
35
43
|
addRecordLinkTemplate(pNameTemplate, pURLTemplate, pDefault)
|
|
36
44
|
{
|
|
37
45
|
let tmpDefaultAction = (typeof(pDefault) !== 'undefined') ? pDefault : false;
|
|
@@ -8,7 +8,7 @@ const libPictProvider = require('pict-provider');
|
|
|
8
8
|
const _DefaultProviderConfiguration = {
|
|
9
9
|
ProviderIdentifier: 'Pict-RecordSetProvider',
|
|
10
10
|
AutoInitialize: true,
|
|
11
|
-
AutoInitializeOrdinal:
|
|
11
|
+
AutoInitializeOrdinal: 1, // initialize after other providers if initialized during app init
|
|
12
12
|
AutoSolveWithApp: false,
|
|
13
13
|
};
|
|
14
14
|
|
|
@@ -432,6 +432,8 @@ class MeadowEndpointsRecordSetProvider extends libRecordSetProviderBase
|
|
|
432
432
|
{
|
|
433
433
|
super.onInitializeAsync((pError) =>
|
|
434
434
|
{
|
|
435
|
+
//FIXME: load the most recent experience from local storage using the other provider
|
|
436
|
+
// how do we control the initialization order of the providers?
|
|
435
437
|
if (this.options.FilterExperiences && typeof this.options.FilterExperiences === 'object' && !Array.isArray(this.options.FilterExperiences))
|
|
436
438
|
{
|
|
437
439
|
this.pict.Bundle._ActiveFilterState = this.pict.Bundle._ActiveFilterState || {};
|