@memberjunction/ng-dashboards 2.120.0 → 2.122.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/dist/AI/components/agents/agent-configuration.component.d.ts +23 -11
- package/dist/AI/components/agents/agent-configuration.component.d.ts.map +1 -1
- package/dist/AI/components/agents/agent-configuration.component.js +122 -95
- package/dist/AI/components/agents/agent-configuration.component.js.map +1 -1
- package/dist/AI/components/agents/agent-editor.component.js +88 -90
- package/dist/AI/components/agents/agent-editor.component.js.map +1 -1
- package/dist/AI/components/agents/agent-filter-panel.component.js +2 -2
- package/dist/AI/components/execution-monitoring.component.d.ts +23 -10
- package/dist/AI/components/execution-monitoring.component.d.ts.map +1 -1
- package/dist/AI/components/execution-monitoring.component.js +143 -124
- package/dist/AI/components/execution-monitoring.component.js.map +1 -1
- package/dist/AI/components/models/model-management-v2.component.d.ts +17 -13
- package/dist/AI/components/models/model-management-v2.component.d.ts.map +1 -1
- package/dist/AI/components/models/model-management-v2.component.js +248 -266
- package/dist/AI/components/models/model-management-v2.component.js.map +1 -1
- package/dist/AI/components/prompts/model-prompt-priority-matrix.component.js +76 -78
- package/dist/AI/components/prompts/model-prompt-priority-matrix.component.js.map +1 -1
- package/dist/AI/components/prompts/prompt-filter-panel.component.js +2 -2
- package/dist/AI/components/prompts/prompt-management-v2.component.d.ts +17 -15
- package/dist/AI/components/prompts/prompt-management-v2.component.d.ts.map +1 -1
- package/dist/AI/components/prompts/prompt-management-v2.component.js +372 -397
- package/dist/AI/components/prompts/prompt-management-v2.component.js.map +1 -1
- package/dist/AI/components/prompts/prompt-version-control.component.js +100 -102
- package/dist/AI/components/prompts/prompt-version-control.component.js.map +1 -1
- package/dist/AI/components/system/system-config-filter-panel.component.js +2 -2
- package/dist/AI/components/system/system-configuration.component.d.ts +17 -10
- package/dist/AI/components/system/system-configuration.component.d.ts.map +1 -1
- package/dist/AI/components/system/system-configuration.component.js +82 -61
- package/dist/AI/components/system/system-configuration.component.js.map +1 -1
- package/dist/AI/components/widgets/kpi-card.component.d.ts.map +1 -1
- package/dist/AI/components/widgets/kpi-card.component.js +11 -7
- package/dist/AI/components/widgets/kpi-card.component.js.map +1 -1
- package/dist/AI/index.d.ts +4 -0
- package/dist/AI/index.d.ts.map +1 -1
- package/dist/AI/index.js +6 -1
- package/dist/AI/index.js.map +1 -1
- package/dist/Actions/components/actions-list-view.component.js +9 -9
- package/dist/Actions/components/actions-list-view.component.js.map +1 -1
- package/dist/Actions/components/actions-overview.component.d.ts +16 -13
- package/dist/Actions/components/actions-overview.component.d.ts.map +1 -1
- package/dist/Actions/components/actions-overview.component.js +62 -48
- package/dist/Actions/components/actions-overview.component.js.map +1 -1
- package/dist/Actions/components/categories-list-view.component.js +9 -9
- package/dist/Actions/components/categories-list-view.component.js.map +1 -1
- package/dist/Actions/components/code-management.component.d.ts +17 -7
- package/dist/Actions/components/code-management.component.d.ts.map +1 -1
- package/dist/Actions/components/code-management.component.js +45 -12
- package/dist/Actions/components/code-management.component.js.map +1 -1
- package/dist/Actions/components/entity-integration.component.d.ts +17 -7
- package/dist/Actions/components/entity-integration.component.d.ts.map +1 -1
- package/dist/Actions/components/entity-integration.component.js +45 -12
- package/dist/Actions/components/entity-integration.component.js.map +1 -1
- package/dist/Actions/components/execution-monitoring.component.d.ts +16 -10
- package/dist/Actions/components/execution-monitoring.component.d.ts.map +1 -1
- package/dist/Actions/components/execution-monitoring.component.js +56 -30
- package/dist/Actions/components/execution-monitoring.component.js.map +1 -1
- package/dist/Actions/components/scheduled-actions.component.d.ts +17 -7
- package/dist/Actions/components/scheduled-actions.component.d.ts.map +1 -1
- package/dist/Actions/components/scheduled-actions.component.js +45 -12
- package/dist/Actions/components/scheduled-actions.component.js.map +1 -1
- package/dist/Actions/components/security-permissions.component.d.ts +17 -7
- package/dist/Actions/components/security-permissions.component.d.ts.map +1 -1
- package/dist/Actions/components/security-permissions.component.js +45 -12
- package/dist/Actions/components/security-permissions.component.js.map +1 -1
- package/dist/Actions/index.d.ts +6 -1
- package/dist/Actions/index.d.ts.map +1 -1
- package/dist/Actions/index.js +9 -1
- package/dist/Actions/index.js.map +1 -1
- package/dist/ComponentStudio/component-studio-dashboard.component.d.ts +1 -1
- package/dist/ComponentStudio/component-studio-dashboard.component.js +8 -8
- package/dist/ComponentStudio/component-studio-dashboard.component.js.map +1 -1
- package/dist/ComponentStudio/components/artifact-load-dialog.component.js +52 -57
- package/dist/ComponentStudio/components/artifact-load-dialog.component.js.map +1 -1
- package/dist/ComponentStudio/components/artifact-selection-dialog.component.js +8 -9
- package/dist/ComponentStudio/components/artifact-selection-dialog.component.js.map +1 -1
- package/dist/DataExplorer/components/navigation-panel/navigation-panel.component.d.ts +107 -0
- package/dist/DataExplorer/components/navigation-panel/navigation-panel.component.d.ts.map +1 -0
- package/dist/DataExplorer/components/navigation-panel/navigation-panel.component.js +553 -0
- package/dist/DataExplorer/components/navigation-panel/navigation-panel.component.js.map +1 -0
- package/dist/DataExplorer/components/view-config-panel/view-config-panel.component.d.ts +179 -0
- package/dist/DataExplorer/components/view-config-panel/view-config-panel.component.d.ts.map +1 -0
- package/dist/DataExplorer/components/view-config-panel/view-config-panel.component.js +814 -0
- package/dist/DataExplorer/components/view-config-panel/view-config-panel.component.js.map +1 -0
- package/dist/DataExplorer/components/view-selector/view-selector.component.d.ts +151 -0
- package/dist/DataExplorer/components/view-selector/view-selector.component.d.ts.map +1 -0
- package/dist/DataExplorer/components/view-selector/view-selector.component.js +480 -0
- package/dist/DataExplorer/components/view-selector/view-selector.component.js.map +1 -0
- package/dist/DataExplorer/data-explorer-dashboard.component.d.ts +439 -0
- package/dist/DataExplorer/data-explorer-dashboard.component.d.ts.map +1 -0
- package/dist/DataExplorer/data-explorer-dashboard.component.js +2129 -0
- package/dist/DataExplorer/data-explorer-dashboard.component.js.map +1 -0
- package/dist/DataExplorer/index.d.ts +5 -0
- package/dist/DataExplorer/index.d.ts.map +1 -0
- package/dist/DataExplorer/index.js +10 -0
- package/dist/DataExplorer/index.js.map +1 -0
- package/dist/DataExplorer/models/explorer-state.interface.d.ts +183 -0
- package/dist/DataExplorer/models/explorer-state.interface.d.ts.map +1 -0
- package/dist/DataExplorer/models/explorer-state.interface.js +31 -0
- package/dist/DataExplorer/models/explorer-state.interface.js.map +1 -0
- package/dist/DataExplorer/services/explorer-state.service.d.ts +232 -0
- package/dist/DataExplorer/services/explorer-state.service.d.ts.map +1 -0
- package/dist/DataExplorer/services/explorer-state.service.js +912 -0
- package/dist/DataExplorer/services/explorer-state.service.js.map +1 -0
- package/dist/EntityAdmin/components/entity-details.component.d.ts.map +1 -1
- package/dist/EntityAdmin/components/entity-details.component.js +11 -13
- package/dist/EntityAdmin/components/entity-details.component.js.map +1 -1
- package/dist/EntityAdmin/components/entity-filter-panel.component.js +2 -2
- package/dist/EntityAdmin/components/erd-composite.component.js +2 -2
- package/dist/EntityAdmin/components/erd-diagram.component.js +2 -2
- package/dist/EntityAdmin/entity-admin-dashboard.component.d.ts +1 -1
- package/dist/EntityAdmin/entity-admin-dashboard.component.d.ts.map +1 -1
- package/dist/EntityAdmin/entity-admin-dashboard.component.js +14 -15
- package/dist/EntityAdmin/entity-admin-dashboard.component.js.map +1 -1
- package/dist/Home/home-dashboard.component.d.ts +122 -0
- package/dist/Home/home-dashboard.component.d.ts.map +1 -0
- package/dist/Home/home-dashboard.component.js +698 -0
- package/dist/Home/home-dashboard.component.js.map +1 -0
- package/dist/Scheduling/components/index.d.ts +11 -0
- package/dist/Scheduling/components/index.d.ts.map +1 -0
- package/dist/Scheduling/components/index.js +13 -0
- package/dist/Scheduling/components/index.js.map +1 -0
- package/dist/Scheduling/components/scheduling-health-resource.component.d.ts +20 -0
- package/dist/Scheduling/components/scheduling-health-resource.component.d.ts.map +1 -0
- package/dist/Scheduling/components/scheduling-health-resource.component.js +55 -0
- package/dist/Scheduling/components/scheduling-health-resource.component.js.map +1 -0
- package/dist/Scheduling/components/scheduling-health.component.js +7 -8
- package/dist/Scheduling/components/scheduling-health.component.js.map +1 -1
- package/dist/Scheduling/components/scheduling-history-resource.component.d.ts +20 -0
- package/dist/Scheduling/components/scheduling-history-resource.component.d.ts.map +1 -0
- package/dist/Scheduling/components/scheduling-history-resource.component.js +55 -0
- package/dist/Scheduling/components/scheduling-history-resource.component.js.map +1 -0
- package/dist/Scheduling/components/scheduling-history.component.js +7 -8
- package/dist/Scheduling/components/scheduling-history.component.js.map +1 -1
- package/dist/Scheduling/components/scheduling-jobs-resource.component.d.ts +20 -0
- package/dist/Scheduling/components/scheduling-jobs-resource.component.d.ts.map +1 -0
- package/dist/Scheduling/components/scheduling-jobs-resource.component.js +55 -0
- package/dist/Scheduling/components/scheduling-jobs-resource.component.js.map +1 -0
- package/dist/Scheduling/components/scheduling-jobs.component.js +7 -8
- package/dist/Scheduling/components/scheduling-jobs.component.js.map +1 -1
- package/dist/Scheduling/components/scheduling-monitor-resource.component.d.ts +20 -0
- package/dist/Scheduling/components/scheduling-monitor-resource.component.d.ts.map +1 -0
- package/dist/Scheduling/components/scheduling-monitor-resource.component.js +55 -0
- package/dist/Scheduling/components/scheduling-monitor-resource.component.js.map +1 -0
- package/dist/Scheduling/components/scheduling-monitoring.component.js +7 -8
- package/dist/Scheduling/components/scheduling-monitoring.component.js.map +1 -1
- package/dist/Scheduling/components/scheduling-types-resource.component.d.ts +20 -0
- package/dist/Scheduling/components/scheduling-types-resource.component.d.ts.map +1 -0
- package/dist/Scheduling/components/scheduling-types-resource.component.js +55 -0
- package/dist/Scheduling/components/scheduling-types-resource.component.js.map +1 -0
- package/dist/Scheduling/components/scheduling-types.component.js +7 -8
- package/dist/Scheduling/components/scheduling-types.component.js.map +1 -1
- package/dist/Scheduling/scheduling-dashboard.component.d.ts +1 -1
- package/dist/Scheduling/scheduling-dashboard.component.js +3 -3
- package/dist/Testing/components/index.d.ts +11 -0
- package/dist/Testing/components/index.d.ts.map +1 -0
- package/dist/Testing/components/index.js +13 -0
- package/dist/Testing/components/index.js.map +1 -0
- package/dist/Testing/components/testing-analytics-resource.component.d.ts +20 -0
- package/dist/Testing/components/testing-analytics-resource.component.d.ts.map +1 -0
- package/dist/Testing/components/testing-analytics-resource.component.js +55 -0
- package/dist/Testing/components/testing-analytics-resource.component.js.map +1 -0
- package/dist/Testing/components/testing-execution-resource.component.d.ts +20 -0
- package/dist/Testing/components/testing-execution-resource.component.d.ts.map +1 -0
- package/dist/Testing/components/testing-execution-resource.component.js +55 -0
- package/dist/Testing/components/testing-execution-resource.component.js.map +1 -0
- package/dist/Testing/components/testing-execution.component.js +3 -3
- package/dist/Testing/components/testing-execution.component.js.map +1 -1
- package/dist/Testing/components/testing-feedback-resource.component.d.ts +20 -0
- package/dist/Testing/components/testing-feedback-resource.component.d.ts.map +1 -0
- package/dist/Testing/components/testing-feedback-resource.component.js +55 -0
- package/dist/Testing/components/testing-feedback-resource.component.js.map +1 -0
- package/dist/Testing/components/testing-overview-resource.component.d.ts +20 -0
- package/dist/Testing/components/testing-overview-resource.component.d.ts.map +1 -0
- package/dist/Testing/components/testing-overview-resource.component.js +55 -0
- package/dist/Testing/components/testing-overview-resource.component.js.map +1 -0
- package/dist/Testing/components/testing-version-resource.component.d.ts +20 -0
- package/dist/Testing/components/testing-version-resource.component.d.ts.map +1 -0
- package/dist/Testing/components/testing-version-resource.component.js +55 -0
- package/dist/Testing/components/testing-version-resource.component.js.map +1 -0
- package/dist/Testing/testing-dashboard.component.d.ts +1 -1
- package/dist/Testing/testing-dashboard.component.js +23 -25
- package/dist/Testing/testing-dashboard.component.js.map +1 -1
- package/dist/module.d.ts +83 -66
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +137 -19
- package/dist/module.js.map +1 -1
- package/dist/public-api.d.ts +6 -4
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +41 -13
- package/dist/public-api.js.map +1 -1
- package/package.json +17 -14
- package/dist/AI/ai-dashboard.component.d.ts +0 -62
- package/dist/AI/ai-dashboard.component.d.ts.map +0 -1
- package/dist/AI/ai-dashboard.component.js +0 -338
- package/dist/AI/ai-dashboard.component.js.map +0 -1
- package/dist/Actions/actions-management-dashboard.component.d.ts +0 -52
- package/dist/Actions/actions-management-dashboard.component.d.ts.map +0 -1
- package/dist/Actions/actions-management-dashboard.component.js +0 -308
- package/dist/Actions/actions-management-dashboard.component.js.map +0 -1
- package/dist/generic/base-dashboard.d.ts +0 -65
- package/dist/generic/base-dashboard.d.ts.map +0 -1
- package/dist/generic/base-dashboard.js +0 -74
- package/dist/generic/base-dashboard.js.map +0 -1
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { BehaviorSubject } from 'rxjs';
|
|
3
|
+
import { Metadata, RunView, CompositeKey } from '@memberjunction/core';
|
|
4
|
+
import { DEFAULT_EXPLORER_STATE } from '../models/explorer-state.interface';
|
|
5
|
+
import * as i0 from "@angular/core";
|
|
6
|
+
const BASE_SETTING_KEY = 'DataExplorer.State';
|
|
7
|
+
const MAX_RECENT_ITEMS = 20;
|
|
8
|
+
const MAX_RECENT_ENTITIES = 10;
|
|
9
|
+
const MAX_RECENT_RECORDS = 10;
|
|
10
|
+
const MAX_ENTITY_CACHE_SIZE = 50; // LRU cache limit
|
|
11
|
+
export class ExplorerStateService {
|
|
12
|
+
state$ = new BehaviorSubject(DEFAULT_EXPLORER_STATE);
|
|
13
|
+
metadata = new Metadata();
|
|
14
|
+
saveTimeout = null;
|
|
15
|
+
/** Current context filter (determines which settings key to use) */
|
|
16
|
+
currentFilter = null;
|
|
17
|
+
/** Computed breadcrumbs based on current state */
|
|
18
|
+
breadcrumbs$ = new BehaviorSubject([]);
|
|
19
|
+
/** Recent records loaded from User Record Logs */
|
|
20
|
+
recentRecords$ = new BehaviorSubject([]);
|
|
21
|
+
/** Favorite records loaded from User Favorites (non-entity favorites) */
|
|
22
|
+
favoriteRecords$ = new BehaviorSubject([]);
|
|
23
|
+
/** Application entities with DefaultForNewUser info (loaded once per context) */
|
|
24
|
+
applicationEntities = [];
|
|
25
|
+
/** Set of entity IDs that have DefaultForNewUser=true */
|
|
26
|
+
defaultEntityIds = new Set();
|
|
27
|
+
constructor() {
|
|
28
|
+
// Don't load state in constructor - wait for setContext to be called
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Set the context filter and load context-specific state.
|
|
32
|
+
* Call this before using the service to ensure proper context isolation.
|
|
33
|
+
*/
|
|
34
|
+
async setContext(filter) {
|
|
35
|
+
this.currentFilter = filter;
|
|
36
|
+
await this.loadState();
|
|
37
|
+
// Load application entities first (needed for filtering recent/favorite records)
|
|
38
|
+
await this.loadApplicationEntities();
|
|
39
|
+
// Then load the rest in parallel
|
|
40
|
+
await Promise.all([
|
|
41
|
+
this.loadFavoriteEntities(),
|
|
42
|
+
this.loadFavoriteRecords(),
|
|
43
|
+
this.loadRecentRecords()
|
|
44
|
+
]);
|
|
45
|
+
this.updateBreadcrumbs();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the storage key based on current context.
|
|
49
|
+
* Uses application ID if available for context-specific storage.
|
|
50
|
+
*/
|
|
51
|
+
getSettingKey() {
|
|
52
|
+
if (this.currentFilter?.applicationId) {
|
|
53
|
+
return `${BASE_SETTING_KEY}.${this.currentFilter.applicationId}`;
|
|
54
|
+
}
|
|
55
|
+
return BASE_SETTING_KEY;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get observable of current state
|
|
59
|
+
*/
|
|
60
|
+
get State() {
|
|
61
|
+
return this.state$.asObservable();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get current state value
|
|
65
|
+
*/
|
|
66
|
+
get CurrentState() {
|
|
67
|
+
return this.state$.value;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Observable of current breadcrumbs
|
|
71
|
+
*/
|
|
72
|
+
get Breadcrumbs() {
|
|
73
|
+
return this.breadcrumbs$.asObservable();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get current breadcrumbs value
|
|
77
|
+
*/
|
|
78
|
+
get CurrentBreadcrumbs() {
|
|
79
|
+
return this.breadcrumbs$.value;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get the current context filter
|
|
83
|
+
*/
|
|
84
|
+
get CurrentFilter() {
|
|
85
|
+
return this.currentFilter;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Observable of recent records (from User Record Logs)
|
|
89
|
+
*/
|
|
90
|
+
get RecentRecords() {
|
|
91
|
+
return this.recentRecords$.asObservable();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get current recent records value
|
|
95
|
+
*/
|
|
96
|
+
get CurrentRecentRecords() {
|
|
97
|
+
return this.recentRecords$.value;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Observable of favorite records (from User Favorites, non-entity)
|
|
101
|
+
*/
|
|
102
|
+
get FavoriteRecords() {
|
|
103
|
+
return this.favoriteRecords$.asObservable();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get current favorite records value
|
|
107
|
+
*/
|
|
108
|
+
get CurrentFavoriteRecords() {
|
|
109
|
+
return this.favoriteRecords$.value;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get the set of entity IDs with DefaultForNewUser=true
|
|
113
|
+
*/
|
|
114
|
+
get DefaultEntityIds() {
|
|
115
|
+
return this.defaultEntityIds;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Update state partially
|
|
119
|
+
*/
|
|
120
|
+
updateState(partial) {
|
|
121
|
+
const newState = { ...this.state$.value, ...partial };
|
|
122
|
+
this.state$.next(newState);
|
|
123
|
+
this.updateBreadcrumbs();
|
|
124
|
+
this.debouncedSave();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Set selected entity
|
|
128
|
+
* Caches current entity's filter before switching, restores new entity's cached filter
|
|
129
|
+
*/
|
|
130
|
+
selectEntity(entityName) {
|
|
131
|
+
const currentState = this.state$.value;
|
|
132
|
+
const entityCache = { ...currentState.entityCache };
|
|
133
|
+
// Cache current entity's filter before switching (if we have a current entity)
|
|
134
|
+
if (currentState.selectedEntityName && currentState.smartFilterPrompt) {
|
|
135
|
+
entityCache[currentState.selectedEntityName] = {
|
|
136
|
+
filterText: currentState.smartFilterPrompt,
|
|
137
|
+
lastAccessed: Date.now()
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Restore filter for new entity (if cached) or clear it
|
|
141
|
+
let restoredFilter = '';
|
|
142
|
+
if (entityName && entityCache[entityName]) {
|
|
143
|
+
restoredFilter = entityCache[entityName].filterText;
|
|
144
|
+
entityCache[entityName].lastAccessed = Date.now();
|
|
145
|
+
}
|
|
146
|
+
// Evict old entries if cache is too large (LRU)
|
|
147
|
+
this.evictOldCacheEntries(entityCache);
|
|
148
|
+
this.updateState({
|
|
149
|
+
selectedEntityName: entityName,
|
|
150
|
+
selectedViewId: null, // Reset view when entity changes
|
|
151
|
+
smartFilterPrompt: restoredFilter, // Restore cached filter or empty
|
|
152
|
+
selectedRecordId: null,
|
|
153
|
+
detailPanelOpen: false,
|
|
154
|
+
entityCache
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Evict oldest cache entries if over limit (LRU eviction)
|
|
159
|
+
*/
|
|
160
|
+
evictOldCacheEntries(cache) {
|
|
161
|
+
const entries = Object.entries(cache);
|
|
162
|
+
if (entries.length <= MAX_ENTITY_CACHE_SIZE)
|
|
163
|
+
return;
|
|
164
|
+
// Sort by lastAccessed (oldest first) and remove excess
|
|
165
|
+
entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed);
|
|
166
|
+
const toRemove = entries.length - MAX_ENTITY_CACHE_SIZE;
|
|
167
|
+
for (let i = 0; i < toRemove; i++) {
|
|
168
|
+
delete cache[entries[i][0]];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Set selected view
|
|
173
|
+
*/
|
|
174
|
+
selectView(viewId) {
|
|
175
|
+
this.updateState({
|
|
176
|
+
selectedViewId: viewId,
|
|
177
|
+
viewModified: false // Reset modified state when selecting a different view
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Mark the current view as modified (has unsaved changes)
|
|
182
|
+
*/
|
|
183
|
+
setViewModified(modified) {
|
|
184
|
+
this.updateState({ viewModified: modified });
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Toggle the view configuration panel
|
|
188
|
+
*/
|
|
189
|
+
toggleViewConfigPanel() {
|
|
190
|
+
this.updateState({ viewConfigPanelOpen: !this.state$.value.viewConfigPanelOpen });
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Open the view configuration panel
|
|
194
|
+
*/
|
|
195
|
+
openViewConfigPanel() {
|
|
196
|
+
this.updateState({ viewConfigPanelOpen: true });
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Close the view configuration panel
|
|
200
|
+
*/
|
|
201
|
+
closeViewConfigPanel() {
|
|
202
|
+
this.updateState({ viewConfigPanelOpen: false });
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Set view mode
|
|
206
|
+
*/
|
|
207
|
+
setViewMode(mode) {
|
|
208
|
+
this.updateState({ viewMode: mode });
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Set smart filter prompt
|
|
212
|
+
* Also updates the entity cache for the current entity
|
|
213
|
+
*/
|
|
214
|
+
setSmartFilterPrompt(prompt) {
|
|
215
|
+
const currentState = this.state$.value;
|
|
216
|
+
const entityCache = { ...currentState.entityCache };
|
|
217
|
+
// Update cache for current entity
|
|
218
|
+
if (currentState.selectedEntityName) {
|
|
219
|
+
if (prompt) {
|
|
220
|
+
entityCache[currentState.selectedEntityName] = {
|
|
221
|
+
filterText: prompt,
|
|
222
|
+
lastAccessed: Date.now()
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// Clear cache entry if filter is empty
|
|
227
|
+
delete entityCache[currentState.selectedEntityName];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
this.updateState({ smartFilterPrompt: prompt, entityCache });
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Select a record and open detail panel
|
|
234
|
+
* @param recordId The composite key string for the record
|
|
235
|
+
* @param recordName Optional display name for the record (for breadcrumbs)
|
|
236
|
+
*/
|
|
237
|
+
selectRecord(recordId, recordName) {
|
|
238
|
+
this.updateState({
|
|
239
|
+
selectedRecordId: recordId,
|
|
240
|
+
selectedRecordName: recordName || null,
|
|
241
|
+
detailPanelOpen: recordId !== null
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Close detail panel
|
|
246
|
+
*/
|
|
247
|
+
closeDetailPanel() {
|
|
248
|
+
this.updateState({
|
|
249
|
+
detailPanelOpen: false,
|
|
250
|
+
selectedRecordId: null,
|
|
251
|
+
selectedRecordName: null
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Toggle navigation panel collapsed state
|
|
256
|
+
*/
|
|
257
|
+
toggleNavigationPanel() {
|
|
258
|
+
this.updateState({
|
|
259
|
+
navigationPanelCollapsed: !this.state$.value.navigationPanelCollapsed
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Expand navigation panel and ensure a specific section is expanded
|
|
264
|
+
*/
|
|
265
|
+
expandAndFocusSection(section) {
|
|
266
|
+
const updates = {
|
|
267
|
+
navigationPanelCollapsed: false
|
|
268
|
+
};
|
|
269
|
+
// Ensure the target section is expanded
|
|
270
|
+
switch (section) {
|
|
271
|
+
case 'favorites':
|
|
272
|
+
updates.favoritesSectionExpanded = true;
|
|
273
|
+
break;
|
|
274
|
+
case 'recent':
|
|
275
|
+
updates.recentSectionExpanded = true;
|
|
276
|
+
break;
|
|
277
|
+
case 'entities':
|
|
278
|
+
updates.entitiesSectionExpanded = true;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
this.updateState(updates);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Add item to recent history
|
|
285
|
+
*/
|
|
286
|
+
addRecentItem(item) {
|
|
287
|
+
const recentItems = [...this.state$.value.recentItems];
|
|
288
|
+
// Remove existing entry if present (match by entity and composite key)
|
|
289
|
+
const existingIndex = recentItems.findIndex(r => r.entityName === item.entityName && r.compositeKeyString === item.compositeKeyString);
|
|
290
|
+
if (existingIndex >= 0) {
|
|
291
|
+
recentItems.splice(existingIndex, 1);
|
|
292
|
+
}
|
|
293
|
+
// Add to front with timestamp
|
|
294
|
+
recentItems.unshift({
|
|
295
|
+
...item,
|
|
296
|
+
timestamp: new Date()
|
|
297
|
+
});
|
|
298
|
+
// Trim to max size
|
|
299
|
+
if (recentItems.length > MAX_RECENT_ITEMS) {
|
|
300
|
+
recentItems.length = MAX_RECENT_ITEMS;
|
|
301
|
+
}
|
|
302
|
+
this.updateState({ recentItems });
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Add item to favorites
|
|
306
|
+
*/
|
|
307
|
+
addFavorite(item) {
|
|
308
|
+
const favorites = [...this.state$.value.favorites];
|
|
309
|
+
// Check if already exists
|
|
310
|
+
const exists = favorites.some(f => f.type === item.type &&
|
|
311
|
+
f.entityName === item.entityName &&
|
|
312
|
+
f.compositeKeyString === item.compositeKeyString &&
|
|
313
|
+
f.viewId === item.viewId);
|
|
314
|
+
if (!exists) {
|
|
315
|
+
favorites.push(item);
|
|
316
|
+
this.updateState({ favorites });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Remove item from favorites
|
|
321
|
+
*/
|
|
322
|
+
removeFavorite(item) {
|
|
323
|
+
const favorites = this.state$.value.favorites.filter(f => !(f.type === item.type &&
|
|
324
|
+
f.entityName === item.entityName &&
|
|
325
|
+
f.compositeKeyString === item.compositeKeyString &&
|
|
326
|
+
f.viewId === item.viewId));
|
|
327
|
+
this.updateState({ favorites });
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Toggle section expanded state
|
|
331
|
+
*/
|
|
332
|
+
toggleSection(section) {
|
|
333
|
+
const key = `${section}SectionExpanded`;
|
|
334
|
+
this.updateState({ [key]: !this.state$.value[key] });
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Load state from UserSetting
|
|
338
|
+
*/
|
|
339
|
+
async loadState() {
|
|
340
|
+
try {
|
|
341
|
+
const userId = this.metadata.CurrentUser?.ID;
|
|
342
|
+
if (!userId)
|
|
343
|
+
return;
|
|
344
|
+
const settingKey = this.getSettingKey();
|
|
345
|
+
const rv = new RunView();
|
|
346
|
+
const result = await rv.RunView({
|
|
347
|
+
EntityName: 'MJ: User Settings',
|
|
348
|
+
ExtraFilter: `UserID='${userId}' AND Setting='${settingKey}'`,
|
|
349
|
+
ResultType: 'entity_object'
|
|
350
|
+
});
|
|
351
|
+
if (result.Success && result.Results.length > 0) {
|
|
352
|
+
const setting = result.Results[0];
|
|
353
|
+
if (setting.Value) {
|
|
354
|
+
const savedState = JSON.parse(setting.Value);
|
|
355
|
+
// Merge with defaults to handle new properties
|
|
356
|
+
this.state$.next({ ...DEFAULT_EXPLORER_STATE, ...savedState });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// No saved state, use defaults
|
|
361
|
+
this.state$.next({ ...DEFAULT_EXPLORER_STATE });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
console.warn('Failed to load Data Explorer state:', error);
|
|
366
|
+
this.state$.next({ ...DEFAULT_EXPLORER_STATE });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Update breadcrumbs based on current state and filter
|
|
371
|
+
*/
|
|
372
|
+
updateBreadcrumbs() {
|
|
373
|
+
const breadcrumbs = [];
|
|
374
|
+
const state = this.state$.value;
|
|
375
|
+
// Always add a root/home breadcrumb - use application name if available, otherwise "All"
|
|
376
|
+
const rootLabel = this.currentFilter?.applicationName || 'All';
|
|
377
|
+
breadcrumbs.push({
|
|
378
|
+
label: rootLabel,
|
|
379
|
+
type: 'application',
|
|
380
|
+
icon: 'fa-solid fa-th-large'
|
|
381
|
+
});
|
|
382
|
+
// Add entity breadcrumb if selected
|
|
383
|
+
if (state.selectedEntityName) {
|
|
384
|
+
const entityInfo = this.metadata.Entities.find(e => e.Name === state.selectedEntityName);
|
|
385
|
+
breadcrumbs.push({
|
|
386
|
+
label: state.selectedEntityName,
|
|
387
|
+
type: 'entity',
|
|
388
|
+
entityName: state.selectedEntityName,
|
|
389
|
+
icon: entityInfo?.Icon ? this.formatEntityIcon(entityInfo.Icon) : 'fa-solid fa-table'
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
// Add record breadcrumb if selected
|
|
393
|
+
if (state.selectedRecordId && state.selectedRecordName) {
|
|
394
|
+
breadcrumbs.push({
|
|
395
|
+
label: state.selectedRecordName,
|
|
396
|
+
type: 'record',
|
|
397
|
+
entityName: state.selectedEntityName || undefined,
|
|
398
|
+
compositeKeyString: state.selectedRecordId,
|
|
399
|
+
icon: 'fa-solid fa-file-alt'
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
this.breadcrumbs$.next(breadcrumbs);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Format entity icon to ensure proper Font Awesome class format
|
|
406
|
+
*/
|
|
407
|
+
formatEntityIcon(icon) {
|
|
408
|
+
if (!icon)
|
|
409
|
+
return 'fa-solid fa-table';
|
|
410
|
+
if (icon.startsWith('fa-') || icon.startsWith('fa ')) {
|
|
411
|
+
if (icon.startsWith('fa-solid') || icon.startsWith('fa-regular') ||
|
|
412
|
+
icon.startsWith('fa-light') || icon.startsWith('fa-brands') ||
|
|
413
|
+
icon.startsWith('fa ')) {
|
|
414
|
+
return icon;
|
|
415
|
+
}
|
|
416
|
+
return `fa-solid ${icon}`;
|
|
417
|
+
}
|
|
418
|
+
return `fa-solid fa-${icon}`;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Debounced save to avoid too many writes
|
|
422
|
+
*/
|
|
423
|
+
debouncedSave() {
|
|
424
|
+
if (this.saveTimeout) {
|
|
425
|
+
clearTimeout(this.saveTimeout);
|
|
426
|
+
}
|
|
427
|
+
this.saveTimeout = setTimeout(() => {
|
|
428
|
+
this.saveState();
|
|
429
|
+
}, 1000);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Save state to UserSetting
|
|
433
|
+
*/
|
|
434
|
+
async saveState() {
|
|
435
|
+
try {
|
|
436
|
+
const userId = this.metadata.CurrentUser?.ID;
|
|
437
|
+
if (!userId)
|
|
438
|
+
return;
|
|
439
|
+
const settingKey = this.getSettingKey();
|
|
440
|
+
const md = new Metadata();
|
|
441
|
+
const rv = new RunView();
|
|
442
|
+
// Check if setting exists
|
|
443
|
+
const result = await rv.RunView({
|
|
444
|
+
EntityName: 'MJ: User Settings',
|
|
445
|
+
ExtraFilter: `UserID='${userId}' AND Setting='${settingKey}'`,
|
|
446
|
+
ResultType: 'entity_object'
|
|
447
|
+
});
|
|
448
|
+
let setting;
|
|
449
|
+
if (result.Success && result.Results.length > 0) {
|
|
450
|
+
setting = result.Results[0];
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
setting = await md.GetEntityObject('MJ: User Settings');
|
|
454
|
+
setting.UserID = userId;
|
|
455
|
+
setting.Setting = settingKey;
|
|
456
|
+
}
|
|
457
|
+
setting.Value = JSON.stringify(this.state$.value);
|
|
458
|
+
await setting.Save();
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
console.warn('Failed to save Data Explorer state:', error);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Navigate to a specific breadcrumb level.
|
|
466
|
+
* Clears any selection deeper than the clicked breadcrumb.
|
|
467
|
+
*/
|
|
468
|
+
navigateToBreadcrumb(breadcrumb) {
|
|
469
|
+
switch (breadcrumb.type) {
|
|
470
|
+
case 'application':
|
|
471
|
+
// Clear entity and record selection (show home view)
|
|
472
|
+
this.updateState({
|
|
473
|
+
selectedEntityName: null,
|
|
474
|
+
selectedViewId: null,
|
|
475
|
+
selectedRecordId: null,
|
|
476
|
+
selectedRecordName: null,
|
|
477
|
+
smartFilterPrompt: '',
|
|
478
|
+
detailPanelOpen: false
|
|
479
|
+
});
|
|
480
|
+
break;
|
|
481
|
+
case 'entity':
|
|
482
|
+
// Keep entity selected, clear record selection
|
|
483
|
+
if (breadcrumb.entityName) {
|
|
484
|
+
this.updateState({
|
|
485
|
+
selectedRecordId: null,
|
|
486
|
+
selectedRecordName: null,
|
|
487
|
+
detailPanelOpen: false
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
break;
|
|
491
|
+
case 'record':
|
|
492
|
+
// Already at record level, do nothing
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// ========================================
|
|
497
|
+
// HOME SCREEN ENTITY MANAGEMENT
|
|
498
|
+
// ========================================
|
|
499
|
+
/**
|
|
500
|
+
* Toggle between showing all entities or just common (DefaultForNewUser) entities
|
|
501
|
+
*/
|
|
502
|
+
toggleShowAllEntities() {
|
|
503
|
+
this.updateState({ showAllEntities: !this.state$.value.showAllEntities });
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Track entity access - called when user navigates to an entity
|
|
507
|
+
*/
|
|
508
|
+
trackEntityAccess(entityName, entityId) {
|
|
509
|
+
const recentEntityAccesses = [...this.state$.value.recentEntityAccesses];
|
|
510
|
+
// Find existing entry
|
|
511
|
+
const existingIndex = recentEntityAccesses.findIndex(r => r.entityId === entityId);
|
|
512
|
+
if (existingIndex >= 0) {
|
|
513
|
+
// Update existing entry
|
|
514
|
+
recentEntityAccesses[existingIndex] = {
|
|
515
|
+
...recentEntityAccesses[existingIndex],
|
|
516
|
+
lastAccessed: new Date(),
|
|
517
|
+
accessCount: recentEntityAccesses[existingIndex].accessCount + 1
|
|
518
|
+
};
|
|
519
|
+
// Move to front
|
|
520
|
+
const [item] = recentEntityAccesses.splice(existingIndex, 1);
|
|
521
|
+
recentEntityAccesses.unshift(item);
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
// Add new entry at front
|
|
525
|
+
recentEntityAccesses.unshift({
|
|
526
|
+
entityName,
|
|
527
|
+
entityId,
|
|
528
|
+
lastAccessed: new Date(),
|
|
529
|
+
accessCount: 1
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
// Trim to max size
|
|
533
|
+
if (recentEntityAccesses.length > MAX_RECENT_ENTITIES) {
|
|
534
|
+
recentEntityAccesses.length = MAX_RECENT_ENTITIES;
|
|
535
|
+
}
|
|
536
|
+
this.updateState({ recentEntityAccesses });
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Add entity to favorites using User Favorites entity
|
|
540
|
+
*/
|
|
541
|
+
async addEntityToFavorites(entityName, entityId) {
|
|
542
|
+
try {
|
|
543
|
+
const userId = this.metadata.CurrentUser?.ID;
|
|
544
|
+
if (!userId)
|
|
545
|
+
return false;
|
|
546
|
+
// Check if already favorited
|
|
547
|
+
const favorites = this.state$.value.favoriteEntities;
|
|
548
|
+
if (favorites.some(f => f.entityId === entityId)) {
|
|
549
|
+
return true; // Already favorited
|
|
550
|
+
}
|
|
551
|
+
// Get the Entities entity ID for favoriting
|
|
552
|
+
const entitiesEntity = this.metadata.Entities.find(e => e.Name === 'Entities');
|
|
553
|
+
if (!entitiesEntity)
|
|
554
|
+
return false;
|
|
555
|
+
// Create User Favorite record
|
|
556
|
+
const md = new Metadata();
|
|
557
|
+
const favorite = await md.GetEntityObject('User Favorites');
|
|
558
|
+
favorite.UserID = userId;
|
|
559
|
+
favorite.EntityID = entitiesEntity.ID;
|
|
560
|
+
favorite.RecordID = entityId;
|
|
561
|
+
const saved = await favorite.Save();
|
|
562
|
+
if (saved) {
|
|
563
|
+
// Update local state
|
|
564
|
+
const favoriteEntities = [...favorites, {
|
|
565
|
+
userFavoriteId: favorite.ID,
|
|
566
|
+
entityName,
|
|
567
|
+
entityId
|
|
568
|
+
}];
|
|
569
|
+
this.updateState({ favoriteEntities });
|
|
570
|
+
}
|
|
571
|
+
return saved;
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
console.warn('Failed to add entity to favorites:', error);
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Remove entity from favorites
|
|
580
|
+
*/
|
|
581
|
+
async removeEntityFromFavorites(entityId) {
|
|
582
|
+
try {
|
|
583
|
+
const favorites = this.state$.value.favoriteEntities;
|
|
584
|
+
const favorite = favorites.find(f => f.entityId === entityId);
|
|
585
|
+
if (!favorite)
|
|
586
|
+
return true; // Already not favorited
|
|
587
|
+
// Delete User Favorite record
|
|
588
|
+
const md = new Metadata();
|
|
589
|
+
const favoriteEntity = await md.GetEntityObject('User Favorites');
|
|
590
|
+
await favoriteEntity.Load(favorite.userFavoriteId);
|
|
591
|
+
const deleted = await favoriteEntity.Delete();
|
|
592
|
+
if (deleted) {
|
|
593
|
+
// Update local state
|
|
594
|
+
const favoriteEntities = favorites.filter(f => f.entityId !== entityId);
|
|
595
|
+
this.updateState({ favoriteEntities });
|
|
596
|
+
}
|
|
597
|
+
return deleted;
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
console.warn('Failed to remove entity from favorites:', error);
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Check if an entity is favorited
|
|
606
|
+
*/
|
|
607
|
+
isEntityFavorited(entityId) {
|
|
608
|
+
return this.state$.value.favoriteEntities.some(f => f.entityId === entityId);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Load application entities with DefaultForNewUser information
|
|
612
|
+
*/
|
|
613
|
+
async loadApplicationEntities() {
|
|
614
|
+
try {
|
|
615
|
+
if (!this.currentFilter?.applicationId) {
|
|
616
|
+
this.applicationEntities = [];
|
|
617
|
+
this.defaultEntityIds.clear();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const rv = new RunView();
|
|
621
|
+
const result = await rv.RunView({
|
|
622
|
+
EntityName: 'Application Entities',
|
|
623
|
+
ExtraFilter: `ApplicationID = '${this.currentFilter.applicationId}'`,
|
|
624
|
+
ResultType: 'entity_object'
|
|
625
|
+
});
|
|
626
|
+
if (result.Success && result.Results) {
|
|
627
|
+
this.applicationEntities = result.Results;
|
|
628
|
+
this.defaultEntityIds.clear();
|
|
629
|
+
for (const appEntity of result.Results) {
|
|
630
|
+
if (appEntity.DefaultForNewUser) {
|
|
631
|
+
this.defaultEntityIds.add(appEntity.EntityID);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
console.warn('Failed to load application entities:', error);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Load favorite entities from User Favorites
|
|
642
|
+
*/
|
|
643
|
+
async loadFavoriteEntities() {
|
|
644
|
+
try {
|
|
645
|
+
const userId = this.metadata.CurrentUser?.ID;
|
|
646
|
+
if (!userId)
|
|
647
|
+
return;
|
|
648
|
+
// Get the Entities entity to filter favorites
|
|
649
|
+
const entitiesEntity = this.metadata.Entities.find(e => e.Name === 'Entities');
|
|
650
|
+
if (!entitiesEntity)
|
|
651
|
+
return;
|
|
652
|
+
const rv = new RunView();
|
|
653
|
+
const result = await rv.RunView({
|
|
654
|
+
EntityName: 'User Favorites',
|
|
655
|
+
ExtraFilter: `UserID='${userId}' AND EntityID='${entitiesEntity.ID}'`,
|
|
656
|
+
ResultType: 'entity_object'
|
|
657
|
+
});
|
|
658
|
+
if (result.Success && result.Results) {
|
|
659
|
+
const favoriteEntities = [];
|
|
660
|
+
for (const fav of result.Results) {
|
|
661
|
+
// Look up entity name from RecordID (which is the Entity.ID)
|
|
662
|
+
const entity = this.metadata.Entities.find(e => e.ID === fav.RecordID);
|
|
663
|
+
if (entity) {
|
|
664
|
+
favoriteEntities.push({
|
|
665
|
+
userFavoriteId: fav.ID,
|
|
666
|
+
entityName: entity.Name,
|
|
667
|
+
entityId: fav.RecordID
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
this.updateState({ favoriteEntities });
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
console.warn('Failed to load favorite entities:', error);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Load recent records from User Record Logs with batch record name lookup
|
|
680
|
+
*/
|
|
681
|
+
async loadRecentRecords() {
|
|
682
|
+
try {
|
|
683
|
+
const userId = this.metadata.CurrentUser?.ID;
|
|
684
|
+
if (!userId)
|
|
685
|
+
return;
|
|
686
|
+
const rv = new RunView();
|
|
687
|
+
const result = await rv.RunView({
|
|
688
|
+
EntityName: 'User Record Logs',
|
|
689
|
+
ExtraFilter: `UserID='${userId}'`,
|
|
690
|
+
OrderBy: 'LatestAt DESC',
|
|
691
|
+
MaxRows: MAX_RECENT_RECORDS,
|
|
692
|
+
ResultType: 'entity_object'
|
|
693
|
+
});
|
|
694
|
+
if (result.Success && result.Results) {
|
|
695
|
+
const recentRecords = [];
|
|
696
|
+
const recordNameInputs = [];
|
|
697
|
+
const recordIndexMap = new Map(); // Map composite key to array index
|
|
698
|
+
for (const log of result.Results) {
|
|
699
|
+
// Look up entity name from EntityID
|
|
700
|
+
const entity = this.metadata.Entities.find(e => e.ID === log.EntityID);
|
|
701
|
+
if (entity) {
|
|
702
|
+
// Filter by application context if applicable
|
|
703
|
+
if (this.currentFilter?.applicationId && !this.applicationEntities.some(ae => ae.EntityID === log.EntityID)) {
|
|
704
|
+
continue; // Skip records from entities not in this application
|
|
705
|
+
}
|
|
706
|
+
const recordAccess = {
|
|
707
|
+
entityName: entity.Name,
|
|
708
|
+
entityId: log.EntityID,
|
|
709
|
+
recordId: log.RecordID,
|
|
710
|
+
latestAt: log.LatestAt,
|
|
711
|
+
totalCount: log.TotalCount
|
|
712
|
+
};
|
|
713
|
+
const index = recentRecords.length;
|
|
714
|
+
recentRecords.push(recordAccess);
|
|
715
|
+
// Build composite key for batch name lookup
|
|
716
|
+
const compositeKey = this.buildCompositeKeyForEntity(entity, log.RecordID);
|
|
717
|
+
if (compositeKey) {
|
|
718
|
+
recordNameInputs.push({ EntityName: entity.Name, CompositeKey: compositeKey });
|
|
719
|
+
recordIndexMap.set(`${entity.Name}|${log.RecordID}`, index);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
// Batch fetch record names
|
|
724
|
+
if (recordNameInputs.length > 0) {
|
|
725
|
+
try {
|
|
726
|
+
const nameResults = await this.metadata.GetEntityRecordNames(recordNameInputs);
|
|
727
|
+
for (const nameResult of nameResults) {
|
|
728
|
+
if (nameResult.Success && nameResult.RecordName) {
|
|
729
|
+
const pkValue = nameResult.CompositeKey.KeyValuePairs[0]?.Value?.toString();
|
|
730
|
+
const key = `${nameResult.EntityName}|${pkValue}`;
|
|
731
|
+
const index = recordIndexMap.get(key);
|
|
732
|
+
if (index !== undefined) {
|
|
733
|
+
recentRecords[index].recordName = nameResult.RecordName;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
catch (nameError) {
|
|
739
|
+
console.warn('Failed to load record names:', nameError);
|
|
740
|
+
// Continue without names - they'll show record IDs
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
this.recentRecords$.next(recentRecords);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
catch (error) {
|
|
747
|
+
console.warn('Failed to load recent records:', error);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Build a CompositeKey for a given entity and record ID.
|
|
752
|
+
* Assumes single primary key field named ID for most entities.
|
|
753
|
+
*/
|
|
754
|
+
buildCompositeKeyForEntity(entity, recordId) {
|
|
755
|
+
try {
|
|
756
|
+
const entityInfo = this.metadata.Entities.find(e => e.Name === entity.Name);
|
|
757
|
+
if (!entityInfo)
|
|
758
|
+
return null;
|
|
759
|
+
const pkFields = entityInfo.PrimaryKeys;
|
|
760
|
+
if (pkFields.length === 0)
|
|
761
|
+
return null;
|
|
762
|
+
const compositeKey = new CompositeKey();
|
|
763
|
+
// For single PK, use the recordId directly
|
|
764
|
+
if (pkFields.length === 1) {
|
|
765
|
+
compositeKey.KeyValuePairs = [{
|
|
766
|
+
FieldName: pkFields[0].Name,
|
|
767
|
+
Value: recordId
|
|
768
|
+
}];
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
// For composite keys, we'd need to parse the recordId
|
|
772
|
+
// For now, assume the recordId is already in the correct format
|
|
773
|
+
// This is a simplified approach - composite keys would need more handling
|
|
774
|
+
compositeKey.KeyValuePairs = [{
|
|
775
|
+
FieldName: pkFields[0].Name,
|
|
776
|
+
Value: recordId
|
|
777
|
+
}];
|
|
778
|
+
}
|
|
779
|
+
return compositeKey;
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Refresh recent records (call after navigating to a record)
|
|
787
|
+
*/
|
|
788
|
+
async refreshRecentRecords() {
|
|
789
|
+
await this.loadRecentRecords();
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Add or update a recent record locally (without DB refresh).
|
|
793
|
+
* Call this when a record is selected to provide instant UI feedback.
|
|
794
|
+
* The DB write happens separately via RecentAccessService.logAccess().
|
|
795
|
+
*
|
|
796
|
+
* @param entityName The entity name
|
|
797
|
+
* @param entityId The entity ID
|
|
798
|
+
* @param recordId The record ID (primary key value)
|
|
799
|
+
* @param recordName Optional display name for the record
|
|
800
|
+
*/
|
|
801
|
+
addLocalRecentRecord(entityName, entityId, recordId, recordName) {
|
|
802
|
+
// Filter by application context if applicable
|
|
803
|
+
if (this.currentFilter?.applicationId && !this.applicationEntities.some(ae => ae.EntityID === entityId)) {
|
|
804
|
+
return; // Don't add records from entities not in this application
|
|
805
|
+
}
|
|
806
|
+
const currentRecords = [...this.recentRecords$.value];
|
|
807
|
+
// Find existing entry
|
|
808
|
+
const existingIndex = currentRecords.findIndex(r => r.entityId === entityId && r.recordId === recordId);
|
|
809
|
+
const newRecord = {
|
|
810
|
+
entityName,
|
|
811
|
+
entityId,
|
|
812
|
+
recordId,
|
|
813
|
+
recordName,
|
|
814
|
+
latestAt: new Date(),
|
|
815
|
+
totalCount: existingIndex >= 0 ? currentRecords[existingIndex].totalCount + 1 : 1
|
|
816
|
+
};
|
|
817
|
+
if (existingIndex >= 0) {
|
|
818
|
+
// Remove existing entry (will be re-added at front)
|
|
819
|
+
currentRecords.splice(existingIndex, 1);
|
|
820
|
+
}
|
|
821
|
+
// Add to front
|
|
822
|
+
currentRecords.unshift(newRecord);
|
|
823
|
+
// Trim to max size
|
|
824
|
+
if (currentRecords.length > MAX_RECENT_RECORDS) {
|
|
825
|
+
currentRecords.length = MAX_RECENT_RECORDS;
|
|
826
|
+
}
|
|
827
|
+
this.recentRecords$.next(currentRecords);
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Load favorite records from User Favorites (non-entity favorites) with batch record name lookup.
|
|
831
|
+
* Filters to only include records from entities in the current application context.
|
|
832
|
+
*/
|
|
833
|
+
async loadFavoriteRecords() {
|
|
834
|
+
try {
|
|
835
|
+
const userId = this.metadata.CurrentUser?.ID;
|
|
836
|
+
if (!userId)
|
|
837
|
+
return;
|
|
838
|
+
// Get the Entities entity to exclude entity favorites
|
|
839
|
+
const entitiesEntity = this.metadata.Entities.find(e => e.Name === 'Entities');
|
|
840
|
+
const entitiesEntityId = entitiesEntity?.ID || '';
|
|
841
|
+
const rv = new RunView();
|
|
842
|
+
const result = await rv.RunView({
|
|
843
|
+
EntityName: 'User Favorites',
|
|
844
|
+
ExtraFilter: `UserID='${userId}' AND EntityID <> '${entitiesEntityId}'`,
|
|
845
|
+
ResultType: 'entity_object'
|
|
846
|
+
});
|
|
847
|
+
if (result.Success && result.Results) {
|
|
848
|
+
const favoriteRecords = [];
|
|
849
|
+
const recordNameInputs = [];
|
|
850
|
+
const recordIndexMap = new Map();
|
|
851
|
+
for (const fav of result.Results) {
|
|
852
|
+
// Look up entity info from the EntityID
|
|
853
|
+
const entity = this.metadata.Entities.find(e => e.ID === fav.EntityID);
|
|
854
|
+
if (entity) {
|
|
855
|
+
// Filter by application context if applicable
|
|
856
|
+
if (this.currentFilter?.applicationId && !this.applicationEntities.some(ae => ae.EntityID === fav.EntityID)) {
|
|
857
|
+
continue; // Skip records from entities not in this application
|
|
858
|
+
}
|
|
859
|
+
const favoriteRecord = {
|
|
860
|
+
userFavoriteId: fav.ID,
|
|
861
|
+
entityName: entity.Name,
|
|
862
|
+
entityId: fav.EntityID,
|
|
863
|
+
recordId: fav.RecordID
|
|
864
|
+
};
|
|
865
|
+
const index = favoriteRecords.length;
|
|
866
|
+
favoriteRecords.push(favoriteRecord);
|
|
867
|
+
// Build composite key for batch name lookup
|
|
868
|
+
const compositeKey = this.buildCompositeKeyForEntity(entity, fav.RecordID);
|
|
869
|
+
if (compositeKey) {
|
|
870
|
+
recordNameInputs.push({ EntityName: entity.Name, CompositeKey: compositeKey });
|
|
871
|
+
recordIndexMap.set(`${entity.Name}|${fav.RecordID}`, index);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// Batch fetch record names
|
|
876
|
+
if (recordNameInputs.length > 0) {
|
|
877
|
+
try {
|
|
878
|
+
const nameResults = await this.metadata.GetEntityRecordNames(recordNameInputs);
|
|
879
|
+
for (const nameResult of nameResults) {
|
|
880
|
+
if (nameResult.Success && nameResult.RecordName) {
|
|
881
|
+
const pkValue = nameResult.CompositeKey.KeyValuePairs[0]?.Value?.toString();
|
|
882
|
+
const key = `${nameResult.EntityName}|${pkValue}`;
|
|
883
|
+
const index = recordIndexMap.get(key);
|
|
884
|
+
if (index !== undefined) {
|
|
885
|
+
favoriteRecords[index].recordName = nameResult.RecordName;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
catch (nameError) {
|
|
891
|
+
console.warn('Failed to load record names for favorites:', nameError);
|
|
892
|
+
// Continue without names - they'll show record IDs
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
this.favoriteRecords$.next(favoriteRecords);
|
|
896
|
+
this.updateState({ favoriteRecords });
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
console.warn('Failed to load favorite records:', error);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
static ɵfac = function ExplorerStateService_Factory(t) { return new (t || ExplorerStateService)(); };
|
|
904
|
+
static ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: ExplorerStateService, factory: ExplorerStateService.ɵfac, providedIn: 'root' });
|
|
905
|
+
}
|
|
906
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ExplorerStateService, [{
|
|
907
|
+
type: Injectable,
|
|
908
|
+
args: [{
|
|
909
|
+
providedIn: 'root'
|
|
910
|
+
}]
|
|
911
|
+
}], () => [], null); })();
|
|
912
|
+
//# sourceMappingURL=explorer-state.service.js.map
|