pict-section-recordset 1.0.43 → 1.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/example_applications/simple_entity/Simple-RecordSet-Application.js +199 -3
  2. package/example_applications/simple_entity/html/index.html +6 -0
  3. package/package.json +2 -2
  4. package/source/providers/Filter-Data-Provider.js +371 -0
  5. package/source/providers/RecordSet-Link-Manager.js +9 -1
  6. package/source/providers/RecordSet-RecordProvider-Base.js +1 -1
  7. package/source/providers/RecordSet-RecordProvider-MeadowEndpoints.js +76 -43
  8. package/source/services/RecordsSet-MetaController.js +48 -1
  9. package/source/templates/Pict-Template-FilterInstanceViews.js +11 -7
  10. package/source/views/RecordSet-Filters.js +78 -45
  11. package/source/views/filters/RecordSet-Filter-Base.js +19 -1
  12. package/source/views/filters/RecordSet-Filter-ExternalJoinSelectedValue.js +270 -0
  13. package/source/views/filters/RecordSet-Filter-ExternalJoinSelectedValueList.js +277 -0
  14. package/source/views/filters/RecordSet-Filter-InternalJoinSelectedValue.js +270 -0
  15. package/source/views/filters/RecordSet-Filter-InternalJoinSelectedValueList.js +277 -0
  16. package/source/views/filters/index.js +4 -0
  17. package/types/providers/Filter-Data-Provider.d.ts +120 -0
  18. package/types/providers/Filter-Data-Provider.d.ts.map +1 -0
  19. package/types/providers/RecordSet-Link-Manager.d.ts +10 -5
  20. package/types/providers/RecordSet-Link-Manager.d.ts.map +1 -1
  21. package/types/providers/RecordSet-RecordProvider-MeadowEndpoints.d.ts +1 -1
  22. package/types/providers/RecordSet-RecordProvider-MeadowEndpoints.d.ts.map +1 -1
  23. package/types/services/RecordsSet-MetaController.d.ts +36 -7
  24. package/types/services/RecordsSet-MetaController.d.ts.map +1 -1
  25. package/types/templates/Pict-Template-FilterInstanceViews.d.ts +1 -1
  26. package/types/templates/Pict-Template-FilterInstanceViews.d.ts.map +1 -1
  27. package/types/views/RecordSet-Filters.d.ts +10 -0
  28. package/types/views/RecordSet-Filters.d.ts.map +1 -1
  29. package/types/views/filters/RecordSet-Filter-Base.d.ts +10 -0
  30. package/types/views/filters/RecordSet-Filter-Base.d.ts.map +1 -1
  31. package/types/views/filters/RecordSet-Filter-ExternalJoinSelectedValue.d.ts +24 -0
  32. package/types/views/filters/RecordSet-Filter-ExternalJoinSelectedValue.d.ts.map +1 -0
  33. package/types/views/filters/RecordSet-Filter-ExternalJoinSelectedValueList.d.ts +24 -0
  34. package/types/views/filters/RecordSet-Filter-ExternalJoinSelectedValueList.d.ts.map +1 -0
  35. package/types/views/filters/RecordSet-Filter-InternalJoinSelectedValue.d.ts +24 -0
  36. package/types/views/filters/RecordSet-Filter-InternalJoinSelectedValue.d.ts.map +1 -0
  37. package/types/views/filters/RecordSet-Filter-InternalJoinSelectedValueList.d.ts +24 -0
  38. package/types/views/filters/RecordSet-Filter-InternalJoinSelectedValueList.d.ts.map +1 -0
  39. 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~}')">--&gt;</button>
20
+ <button onclick="_Pict.views['{~D:Context[0].Hash~}'].handleRemove(event, '{~D:Record.ClauseAddress~}', '{~D:Record.Hash~}')">&lt;--</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,9 +340,10 @@ module.exports.default_configuration.pict_configuration = (
182
340
  {
183
341
  "ExternalJoinBookByAuthor":
184
342
  {
185
- "DisplayName": "Author's Name",
343
+ "Label": "Author's Name",
186
344
  "Type": "ExternalJoinStringMatch",
187
345
  "ExternalFilterByColumns": [ "Name" ],
346
+ "DisplayName": "Author's Name",
188
347
 
189
348
  "CoreConnectionColumn": "IDBook",
190
349
 
@@ -194,6 +353,39 @@ module.exports.default_configuration.pict_configuration = (
194
353
 
195
354
  "ExternalFilterByTable": "Author",
196
355
  "ExternalFilterByTableConnectionColumn": "IDAuthor"
356
+ },
357
+ "ExternalJoinBookBySelectedAuthors":
358
+ {
359
+ "Label": "Authors",
360
+ "Type": "ExternalJoinSelectedValueList",
361
+ "ExternalFilterByColumns": [ "Name", "GUIDAuthor" ],
362
+
363
+ "MaximumSelectedExternalRecords": 5,
364
+ "ExternalRecordDisplayTemplate": "{~D:Record.Data.Name~}",
365
+
366
+ "CoreConnectionColumn": "IDBook",
367
+
368
+ "JoinTable": "BookAuthorJoin",
369
+ "JoinTableExternalConnectionColumn": "IDAuthor",
370
+ "JoinTableCoreConnectionColumn": "IDBook",
371
+
372
+ "ExternalFilterByTable": "Author",
373
+ "ExternalFilterByTableConnectionColumn": "IDAuthor",
374
+ "CustomFilterViewHash": "MyCoolView"
375
+ },
376
+ "InternalJoinBySelectedJoinIDs":
377
+ {
378
+ "Label": "Join IDs (internal test)",
379
+ "Type": "InternalJoinSelectedValue",
380
+ "ExternalFilterByColumns": [ "IDBookAuthorJoin" ],
381
+
382
+ "ExternalRecordDisplayTemplate": "{~D:Record.Data.IDBookAuthorJoin~} [{~D:Record.Data.IDBook~} &lt;-&gt; {~D:Record.Data.IDAuthor~}]",
383
+
384
+ "CoreConnectionColumn": "IDBook",
385
+
386
+ "RemoteTable": "BookAuthorJoin",
387
+ "JoinExternalConnectionColumn": "IDBook",
388
+ "JoinInternalConnectionColumn": "IDBook"
197
389
  }
198
390
  },
199
391
  "FilterCriteria":
@@ -201,7 +393,11 @@ module.exports.default_configuration.pict_configuration = (
201
393
  "FilterRecordsetByBookAndCreateDate":
202
394
  [
203
395
  {
204
- "FilterDefinitionHash": "ExternalJoinBookByAuthor",
396
+ "FilterDefinitionHash": "ExternalJoinBookBySelectedAuthors",
397
+ "FilterByColumn": "IDBook"
398
+ },
399
+ {
400
+ "FilterDefinitionHash": "InternalJoinBySelectedJoinIDs",
205
401
  "FilterByColumn": "IDBook"
206
402
  },
207
403
  {
@@ -236,7 +432,7 @@ module.exports.default_configuration.pict_configuration = (
236
432
  {
237
433
  "Title":
238
434
  [
239
- { "FilterKey": "Title", "ClauseKey": "TitleMatch", "DisplayName": "Book Title Custom Filter", "Type": "StringMatch", "FilterByColumn": "Title", "ExactMatch": true }
435
+ { "FilterKey": "Title", "ClauseKey": "TitleMatch", "Label": "Book Title Custom Filter", "Type": "StringMatch", "FilterByColumn": "Title", "ExactMatch": true }
240
436
  ]
241
437
  },
242
438
 
@@ -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.43",
3
+ "version": "1.0.45",
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.295",
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
- // TODO: Add the ability to add routes that are scoped to particular entity types
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: 0,
11
+ AutoInitializeOrdinal: 1, // initialize after other providers if initialized during app init
12
12
  AutoSolveWithApp: false,
13
13
  };
14
14