@memberjunction/ng-explorer-core 5.37.0 → 5.39.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/generated/lazy-feature-config.d.ts +1 -1
- package/dist/generated/lazy-feature-config.d.ts.map +1 -1
- package/dist/generated/lazy-feature-config.js +4 -2
- package/dist/generated/lazy-feature-config.js.map +1 -1
- package/dist/lib/resource-wrappers/artifact-resource.component.d.ts +10 -0
- package/dist/lib/resource-wrappers/artifact-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/artifact-resource.component.js +15 -5
- package/dist/lib/resource-wrappers/artifact-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/record-resource.component.js +22 -6
- package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
- package/dist/lib/shell/components/header/app-nav.component.d.ts.map +1 -1
- package/dist/lib/shell/components/header/app-nav.component.js +12 -0
- package/dist/lib/shell/components/header/app-nav.component.js.map +1 -1
- package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +24 -5
- package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/component-cache-manager.js +58 -14
- package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts +42 -0
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.js +195 -12
- package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
- package/dist/lib/shell/shell.component.d.ts +5 -4
- package/dist/lib/shell/shell.component.d.ts.map +1 -1
- package/dist/lib/shell/shell.component.js +20 -9
- package/dist/lib/shell/shell.component.js.map +1 -1
- package/dist/lib/single-record/single-record.component.d.ts +31 -37
- package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
- package/dist/lib/single-record/single-record.component.js +64 -306
- package/dist/lib/single-record/single-record.component.js.map +1 -1
- package/package.json +46 -45
|
@@ -30,16 +30,23 @@ export class ComponentCacheManager {
|
|
|
30
30
|
/**
|
|
31
31
|
* Generate a unique cache key from resource identity.
|
|
32
32
|
* This is the ONE canonical key format used by ALL cache operations.
|
|
33
|
+
*
|
|
34
|
+
* `discriminator` is appended into the recordId slot only when recordId is empty.
|
|
35
|
+
* It exists to prevent collisions between distinct "new record" tabs that share
|
|
36
|
+
* an empty recordId — e.g., a new MJ:Companies form and a new MJ:Employees form
|
|
37
|
+
* would otherwise both cache at `appId::RecordResource::__no_record__` and clobber
|
|
38
|
+
* each other. Passing the entity name as discriminator keeps them separate.
|
|
33
39
|
*/
|
|
34
|
-
getCacheKey(resourceType, recordId, appId) {
|
|
35
|
-
const normalizedRecordId = recordId
|
|
40
|
+
getCacheKey(resourceType, recordId, appId, discriminator) {
|
|
41
|
+
const normalizedRecordId = recordId
|
|
42
|
+
|| (discriminator ? `__new__::${discriminator}` : '__no_record__');
|
|
36
43
|
return `${appId}::${resourceType}::${normalizedRecordId}`;
|
|
37
44
|
}
|
|
38
45
|
/**
|
|
39
46
|
* Check if a component exists in cache and is available for reuse.
|
|
40
47
|
*/
|
|
41
|
-
hasAvailableComponent(resourceType, recordId, appId) {
|
|
42
|
-
const key = this.getCacheKey(resourceType, recordId, appId);
|
|
48
|
+
hasAvailableComponent(resourceType, recordId, appId, discriminator) {
|
|
49
|
+
const key = this.getCacheKey(resourceType, recordId, appId, discriminator);
|
|
43
50
|
const info = this.cache.get(key);
|
|
44
51
|
return info !== undefined && !info.isAttached;
|
|
45
52
|
}
|
|
@@ -47,8 +54,8 @@ export class ComponentCacheManager {
|
|
|
47
54
|
* Get a cached component if available (not currently attached).
|
|
48
55
|
* Lookup is by resource identity, not tab ID.
|
|
49
56
|
*/
|
|
50
|
-
getCachedComponent(resourceType, recordId, appId) {
|
|
51
|
-
const key = this.getCacheKey(resourceType, recordId, appId);
|
|
57
|
+
getCachedComponent(resourceType, recordId, appId, discriminator) {
|
|
58
|
+
const key = this.getCacheKey(resourceType, recordId, appId, discriminator);
|
|
52
59
|
const info = this.cache.get(key);
|
|
53
60
|
if (!info) {
|
|
54
61
|
return null;
|
|
@@ -69,13 +76,17 @@ export class ComponentCacheManager {
|
|
|
69
76
|
const resolvedResourceType = resourceData.Configuration?.resourceTypeDriverClass
|
|
70
77
|
|| resourceData.Configuration?.driverClass
|
|
71
78
|
|| resourceData.ResourceType;
|
|
72
|
-
|
|
79
|
+
// Entity name discriminates between distinct "new record" tabs (empty recordId).
|
|
80
|
+
// For non-Records resource types this is undefined, leaving the key unchanged.
|
|
81
|
+
const discriminator = resourceData.Configuration?.Entity;
|
|
82
|
+
const key = this.getCacheKey(resolvedResourceType, resourceData.ResourceRecordID || '', resourceData.Configuration?.applicationId || '', discriminator);
|
|
73
83
|
const info = {
|
|
74
84
|
componentRef,
|
|
75
85
|
wrapperElement,
|
|
76
86
|
resourceType: resolvedResourceType,
|
|
77
87
|
resourceRecordId: resourceData.ResourceRecordID || '',
|
|
78
88
|
applicationId: resourceData.Configuration?.applicationId || '',
|
|
89
|
+
keyDiscriminator: discriminator,
|
|
79
90
|
isAttached: true,
|
|
80
91
|
attachedToTabId: tabId,
|
|
81
92
|
lastUsed: new Date(),
|
|
@@ -87,8 +98,8 @@ export class ComponentCacheManager {
|
|
|
87
98
|
/**
|
|
88
99
|
* Mark a component as attached. Lookup by resource identity.
|
|
89
100
|
*/
|
|
90
|
-
markAsAttached(resourceType, recordId, appId, tabId) {
|
|
91
|
-
const key = this.getCacheKey(resourceType, recordId, appId);
|
|
101
|
+
markAsAttached(resourceType, recordId, appId, tabId, discriminator) {
|
|
102
|
+
const key = this.getCacheKey(resourceType, recordId, appId, discriminator);
|
|
92
103
|
const info = this.cache.get(key);
|
|
93
104
|
if (info) {
|
|
94
105
|
info.isAttached = true;
|
|
@@ -102,8 +113,8 @@ export class ComponentCacheManager {
|
|
|
102
113
|
* This is the ONLY way to detach a component. Both single-resource mode and
|
|
103
114
|
* Golden Layout mode use this same method to ensure consistent cache behavior.
|
|
104
115
|
*/
|
|
105
|
-
markAsDetached(resourceType, recordId, appId) {
|
|
106
|
-
const key = this.getCacheKey(resourceType, recordId, appId);
|
|
116
|
+
markAsDetached(resourceType, recordId, appId, discriminator) {
|
|
117
|
+
const key = this.getCacheKey(resourceType, recordId, appId, discriminator);
|
|
107
118
|
const info = this.cache.get(key);
|
|
108
119
|
if (!info)
|
|
109
120
|
return null;
|
|
@@ -113,6 +124,39 @@ export class ComponentCacheManager {
|
|
|
113
124
|
this.EvictIfNeeded();
|
|
114
125
|
return info;
|
|
115
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Re-key a cached component when its underlying record identity changes.
|
|
129
|
+
*
|
|
130
|
+
* Used when a "new record" component (cached at `recordId = ''`) becomes a saved record
|
|
131
|
+
* (now identified by its new PK). Without re-keying, the next "new record" request would
|
|
132
|
+
* still resolve to this same cache entry, surfacing the previously-saved record on a form
|
|
133
|
+
* the user expects to be blank.
|
|
134
|
+
*
|
|
135
|
+
* Preserves the live component instance — only the cache key + stored identity change.
|
|
136
|
+
* Returns true if a matching entry was found and re-keyed, false otherwise.
|
|
137
|
+
*/
|
|
138
|
+
rekeyComponent(resourceType, oldRecordId, newRecordId, appId, oldDiscriminator, newDiscriminator) {
|
|
139
|
+
const oldKey = this.getCacheKey(resourceType, oldRecordId, appId, oldDiscriminator);
|
|
140
|
+
const info = this.cache.get(oldKey);
|
|
141
|
+
if (!info) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
// Once a record has a real id, the discriminator no longer matters (the id is unique).
|
|
145
|
+
// Default the new discriminator to undefined unless explicitly provided.
|
|
146
|
+
const newKey = this.getCacheKey(resourceType, newRecordId, appId, newDiscriminator);
|
|
147
|
+
if (oldKey === newKey) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
info.resourceRecordId = newRecordId;
|
|
151
|
+
info.keyDiscriminator = newDiscriminator;
|
|
152
|
+
info.lastUsed = new Date();
|
|
153
|
+
if (info.resourceData) {
|
|
154
|
+
info.resourceData.ResourceRecordID = newRecordId;
|
|
155
|
+
}
|
|
156
|
+
this.cache.delete(oldKey);
|
|
157
|
+
this.cache.set(newKey, info);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
116
160
|
/**
|
|
117
161
|
* Find a cached component by tab ID and detach it.
|
|
118
162
|
* This is a convenience wrapper for callers that only know the tab ID
|
|
@@ -125,7 +169,7 @@ export class ComponentCacheManager {
|
|
|
125
169
|
if (!entry)
|
|
126
170
|
return null;
|
|
127
171
|
const [_, info] = entry;
|
|
128
|
-
return this.markAsDetached(info.resourceType, info.resourceRecordId, info.applicationId);
|
|
172
|
+
return this.markAsDetached(info.resourceType, info.resourceRecordId, info.applicationId, info.keyDiscriminator);
|
|
129
173
|
}
|
|
130
174
|
/**
|
|
131
175
|
* Evict least-recently-used detached components when over the limit.
|
|
@@ -156,8 +200,8 @@ export class ComponentCacheManager {
|
|
|
156
200
|
/**
|
|
157
201
|
* Remove and destroy a specific component from cache by resource identity.
|
|
158
202
|
*/
|
|
159
|
-
destroyComponent(resourceType, recordId, appId) {
|
|
160
|
-
const key = this.getCacheKey(resourceType, recordId, appId);
|
|
203
|
+
destroyComponent(resourceType, recordId, appId, discriminator) {
|
|
204
|
+
const key = this.getCacheKey(resourceType, recordId, appId, discriminator);
|
|
161
205
|
const info = this.cache.get(key);
|
|
162
206
|
if (!info)
|
|
163
207
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component-cache-manager.js","sourceRoot":"","sources":["../../../../../src/lib/shell/components/tabs/component-cache-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { ComponentRef, ApplicationRef } from '@angular/core';\nimport { BaseResourceComponent } from '@memberjunction/ng-shared';\nimport { ResourceData } from '@memberjunction/core-entities';\n\n/**\n * Metadata about a cached component\n */\nexport interface CachedComponentInfo {\n // The Angular component reference\n componentRef: ComponentRef<BaseResourceComponent>;\n\n // The wrapper DOM element (for detaching/reattaching)\n wrapperElement: HTMLElement;\n\n // Resource identity (the ONLY key used for cache operations)\n resourceType: string;\n resourceRecordId: string;\n applicationId: string;\n\n // Usage tracking\n isAttached: boolean; // Currently attached to a tab/container?\n attachedToTabId: string | null; // Which tab is it attached to? (metadata only, NOT used for lookup)\n\n // Lifecycle tracking\n lastUsed: Date;\n createdAt: Date;\n\n // Resource data snapshot (for comparison)\n resourceData: ResourceData;\n\n // Saved query params from the tab config at detach time.\n // Restored to the tab config when the component is reattached,\n // so the URL reflects the component's preserved state.\n savedQueryParams?: Record<string, string>;\n\n // Agent context reported by this component via NavigationService.SetAgentContext()\n // Cached so it can be restored when the component becomes active again.\n AgentContext?: Record<string, unknown>;\n\n // Agent client tools registered by this component via NavigationService.SetAgentClientTools()\n // Cached so they can be re-registered when the component becomes active again.\n AgentClientTools?: { Name: string; Description: string; ParameterSchema: Record<string, unknown>; Handler: (params: Record<string, unknown>) => Promise<unknown> }[];\n}\n\n/**\n * Smart component cache manager that preserves component state across tab switches.\n *\n * ALL cache operations use a consistent identity key: `appId::resourceType::recordId`.\n * This key is the same regardless of whether the component is in Golden Layout (tabbed)\n * mode or Single Resource mode, ensuring components are reusable across both modes.\n *\n * The `attachedToTabId` field is metadata for debugging/display — it is NEVER used\n * as a lookup key. This prevents bugs where multiple resources sharing the same tab ID\n * (e.g., nav items within a single-resource app) interfere with each other's cache state.\n *\n * Features:\n * - Caches components by resource identity (appId + resourceType + recordId)\n * - Tracks component usage to prevent double-attachment\n * - Detaches/reattaches DOM elements without destroying Angular components\n * - LRU eviction when detached component count exceeds MaxDetachedComponents\n */\nexport class ComponentCacheManager {\n private cache = new Map<string, CachedComponentInfo>();\n\n /**\n * Maximum number of detached (not currently visible) components to keep\n * cached. When exceeded, least-recently-used detached components are\n * evicted. Set to 0 to disable eviction (legacy behavior). Default: 20.\n */\n public static MaxDetachedComponents: number = 20;\n\n constructor(private appRef: ApplicationRef) {}\n\n /**\n * Generate a unique cache key from resource identity.\n * This is the ONE canonical key format used by ALL cache operations.\n */\n private getCacheKey(resourceType: string, recordId: string, appId: string): string {\n const normalizedRecordId = recordId || '__no_record__';\n return `${appId}::${resourceType}::${normalizedRecordId}`;\n }\n\n /**\n * Check if a component exists in cache and is available for reuse.\n */\n hasAvailableComponent(resourceType: string, recordId: string, appId: string): boolean {\n const key = this.getCacheKey(resourceType, recordId, appId);\n const info = this.cache.get(key);\n return info !== undefined && !info.isAttached;\n }\n\n /**\n * Get a cached component if available (not currently attached).\n * Lookup is by resource identity, not tab ID.\n */\n getCachedComponent(resourceType: string, recordId: string, appId: string): CachedComponentInfo | null {\n const key = this.getCacheKey(resourceType, recordId, appId);\n const info = this.cache.get(key);\n\n if (!info) {\n return null;\n }\n\n // Can only reuse if not currently attached elsewhere\n if (info.isAttached) {\n return null;\n }\n\n return info;\n }\n\n /**\n * Store a component in the cache and mark as attached.\n */\n cacheComponent(\n componentRef: ComponentRef<BaseResourceComponent>,\n wrapperElement: HTMLElement,\n resourceData: ResourceData,\n tabId: string\n ): void {\n // Use driverClass (the actual component class name) as the resourceType for the cache key,\n // NOT resourceData.ResourceType (which is often just \"Custom\" for dashboard resources).\n // This must match the lookup key used in getCachedComponent/markAsAttached/markAsDetached.\n const resolvedResourceType = resourceData.Configuration?.resourceTypeDriverClass\n || resourceData.Configuration?.driverClass\n || resourceData.ResourceType;\n const key = this.getCacheKey(\n resolvedResourceType,\n resourceData.ResourceRecordID || '',\n resourceData.Configuration?.applicationId || ''\n );\n\n const info: CachedComponentInfo = {\n componentRef,\n wrapperElement,\n resourceType: resolvedResourceType,\n resourceRecordId: resourceData.ResourceRecordID || '',\n applicationId: resourceData.Configuration?.applicationId || '',\n isAttached: true,\n attachedToTabId: tabId,\n lastUsed: new Date(),\n createdAt: new Date(),\n resourceData\n };\n\n this.cache.set(key, info);\n }\n\n /**\n * Mark a component as attached. Lookup by resource identity.\n */\n markAsAttached(resourceType: string, recordId: string, appId: string, tabId: string): void {\n const key = this.getCacheKey(resourceType, recordId, appId);\n const info = this.cache.get(key);\n\n if (info) {\n info.isAttached = true;\n info.attachedToTabId = tabId;\n info.lastUsed = new Date();\n }\n }\n\n /**\n * Mark a component as detached (available for reuse). Lookup by resource identity.\n *\n * This is the ONLY way to detach a component. Both single-resource mode and\n * Golden Layout mode use this same method to ensure consistent cache behavior.\n */\n markAsDetached(resourceType: string, recordId: string, appId: string): CachedComponentInfo | null {\n const key = this.getCacheKey(resourceType, recordId, appId);\n const info = this.cache.get(key);\n if (!info) return null;\n\n info.isAttached = false;\n info.attachedToTabId = null;\n info.lastUsed = new Date();\n this.EvictIfNeeded();\n return info;\n }\n\n /**\n * Find a cached component by tab ID and detach it.\n * This is a convenience wrapper for callers that only know the tab ID\n * (e.g., Golden Layout tab close events). It resolves the tab ID to\n * resource identity, then delegates to the identity-based markAsDetached.\n */\n findAndDetachByTabId(tabId: string): CachedComponentInfo | null {\n const entry = Array.from(this.cache.entries())\n .find(([_, info]) => info.attachedToTabId === tabId);\n\n if (!entry) return null;\n\n const [_, info] = entry;\n return this.markAsDetached(info.resourceType, info.resourceRecordId, info.applicationId);\n }\n\n /**\n * Evict least-recently-used detached components when over the limit.\n * Only evicts components that are not currently attached.\n */\n private EvictIfNeeded(): void {\n if (ComponentCacheManager.MaxDetachedComponents <= 0) return;\n\n const detached = Array.from(this.cache.entries())\n .filter(([_, info]) => !info.isAttached)\n .sort((a, b) => a[1].lastUsed.getTime() - b[1].lastUsed.getTime());\n\n while (detached.length > ComponentCacheManager.MaxDetachedComponents) {\n const [key, info] = detached.shift()!;\n this.appRef.detachView(info.componentRef.hostView);\n info.componentRef.destroy();\n this.cache.delete(key);\n }\n }\n\n /**\n * Get component info by tab ID (for finding what's attached to a tab).\n * Uses linear scan since tabId is metadata, not a key.\n */\n getComponentByTabId(tabId: string): CachedComponentInfo | null {\n const entry = Array.from(this.cache.entries())\n .find(([_, info]) => info.attachedToTabId === tabId);\n\n return entry ? entry[1] : null;\n }\n\n /**\n * Remove and destroy a specific component from cache by resource identity.\n */\n destroyComponent(resourceType: string, recordId: string, appId: string): void {\n const key = this.getCacheKey(resourceType, recordId, appId);\n const info = this.cache.get(key);\n\n if (!info) return;\n\n this.appRef.detachView(info.componentRef.hostView);\n info.componentRef.destroy();\n this.cache.delete(key);\n }\n\n /**\n * Remove and destroy component by tab ID (convenience for Golden Layout tab close).\n */\n destroyComponentByTabId(tabId: string): void {\n const entry = Array.from(this.cache.entries())\n .find(([_, info]) => info.attachedToTabId === tabId);\n\n if (!entry) return;\n\n const [key, info] = entry;\n this.appRef.detachView(info.componentRef.hostView);\n info.componentRef.destroy();\n this.cache.delete(key);\n }\n\n /**\n * Clear the entire cache, destroying all components.\n * Call this on user logout or app shutdown.\n */\n clearCache(): void {\n this.cache.forEach(info => {\n this.appRef.detachView(info.componentRef.hostView);\n info.componentRef.destroy();\n });\n this.cache.clear();\n }\n\n /**\n * Selectively clear cached components matching a predicate.\n * Components that match are destroyed; those that don't are kept.\n *\n * Use this for tenant switching: clear org-scoped components while\n * keeping system/global components alive.\n *\n * @param predicate Return true for components that should be destroyed.\n * @returns Number of components destroyed.\n */\n ClearCacheByPredicate(predicate: (info: CachedComponentInfo) => boolean): number {\n let destroyed = 0;\n const toRemove: string[] = [];\n\n this.cache.forEach((info, key) => {\n if (predicate(info)) {\n this.appRef.detachView(info.componentRef.hostView);\n info.componentRef.destroy();\n toRemove.push(key);\n destroyed++;\n }\n });\n\n for (const key of toRemove) {\n this.cache.delete(key);\n }\n\n return destroyed;\n }\n\n /**\n * Get cache statistics for debugging.\n */\n getCacheStats(): {\n total: number;\n attached: number;\n detached: number;\n byResourceType: Map<string, number>;\n } {\n const stats = {\n total: this.cache.size,\n attached: 0,\n detached: 0,\n byResourceType: new Map<string, number>()\n };\n\n this.cache.forEach(info => {\n if (info.isAttached) {\n stats.attached++;\n } else {\n stats.detached++;\n }\n\n const count = stats.byResourceType.get(info.resourceType) || 0;\n stats.byResourceType.set(info.resourceType, count + 1);\n });\n\n return stats;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"component-cache-manager.js","sourceRoot":"","sources":["../../../../../src/lib/shell/components/tabs/component-cache-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { ComponentRef, ApplicationRef } from '@angular/core';\nimport { BaseResourceComponent } from '@memberjunction/ng-shared';\nimport { ResourceData } from '@memberjunction/core-entities';\n\n/**\n * Metadata about a cached component\n */\nexport interface CachedComponentInfo {\n // The Angular component reference\n componentRef: ComponentRef<BaseResourceComponent>;\n\n // The wrapper DOM element (for detaching/reattaching)\n wrapperElement: HTMLElement;\n\n // Resource identity (the ONLY key used for cache operations)\n resourceType: string;\n resourceRecordId: string;\n applicationId: string;\n\n // Optional tiebreaker used when recordId is empty (e.g., \"new record\" tabs for\n // different entities all have recordId='' but must NOT share a cache entry).\n // For entity-record resources this is the entity name; null/undefined for\n // resource types that don't need disambiguation.\n keyDiscriminator?: string;\n\n // Usage tracking\n isAttached: boolean; // Currently attached to a tab/container?\n attachedToTabId: string | null; // Which tab is it attached to? (metadata only, NOT used for lookup)\n\n // Lifecycle tracking\n lastUsed: Date;\n createdAt: Date;\n\n // Resource data snapshot (for comparison)\n resourceData: ResourceData;\n\n // Saved query params from the tab config at detach time.\n // Restored to the tab config when the component is reattached,\n // so the URL reflects the component's preserved state.\n savedQueryParams?: Record<string, string>;\n\n // Agent context reported by this component via NavigationService.SetAgentContext()\n // Cached so it can be restored when the component becomes active again.\n AgentContext?: Record<string, unknown>;\n\n // Agent client tools registered by this component via NavigationService.SetAgentClientTools()\n // Cached so they can be re-registered when the component becomes active again.\n AgentClientTools?: { Name: string; Description: string; ParameterSchema: Record<string, unknown>; Handler: (params: Record<string, unknown>) => Promise<unknown> }[];\n}\n\n/**\n * Smart component cache manager that preserves component state across tab switches.\n *\n * ALL cache operations use a consistent identity key: `appId::resourceType::recordId`.\n * This key is the same regardless of whether the component is in Golden Layout (tabbed)\n * mode or Single Resource mode, ensuring components are reusable across both modes.\n *\n * The `attachedToTabId` field is metadata for debugging/display — it is NEVER used\n * as a lookup key. This prevents bugs where multiple resources sharing the same tab ID\n * (e.g., nav items within a single-resource app) interfere with each other's cache state.\n *\n * Features:\n * - Caches components by resource identity (appId + resourceType + recordId)\n * - Tracks component usage to prevent double-attachment\n * - Detaches/reattaches DOM elements without destroying Angular components\n * - LRU eviction when detached component count exceeds MaxDetachedComponents\n */\nexport class ComponentCacheManager {\n private cache = new Map<string, CachedComponentInfo>();\n\n /**\n * Maximum number of detached (not currently visible) components to keep\n * cached. When exceeded, least-recently-used detached components are\n * evicted. Set to 0 to disable eviction (legacy behavior). Default: 20.\n */\n public static MaxDetachedComponents: number = 20;\n\n constructor(private appRef: ApplicationRef) {}\n\n /**\n * Generate a unique cache key from resource identity.\n * This is the ONE canonical key format used by ALL cache operations.\n *\n * `discriminator` is appended into the recordId slot only when recordId is empty.\n * It exists to prevent collisions between distinct \"new record\" tabs that share\n * an empty recordId — e.g., a new MJ:Companies form and a new MJ:Employees form\n * would otherwise both cache at `appId::RecordResource::__no_record__` and clobber\n * each other. Passing the entity name as discriminator keeps them separate.\n */\n private getCacheKey(resourceType: string, recordId: string, appId: string, discriminator?: string): string {\n const normalizedRecordId = recordId\n || (discriminator ? `__new__::${discriminator}` : '__no_record__');\n return `${appId}::${resourceType}::${normalizedRecordId}`;\n }\n\n /**\n * Check if a component exists in cache and is available for reuse.\n */\n hasAvailableComponent(resourceType: string, recordId: string, appId: string, discriminator?: string): boolean {\n const key = this.getCacheKey(resourceType, recordId, appId, discriminator);\n const info = this.cache.get(key);\n return info !== undefined && !info.isAttached;\n }\n\n /**\n * Get a cached component if available (not currently attached).\n * Lookup is by resource identity, not tab ID.\n */\n getCachedComponent(resourceType: string, recordId: string, appId: string, discriminator?: string): CachedComponentInfo | null {\n const key = this.getCacheKey(resourceType, recordId, appId, discriminator);\n const info = this.cache.get(key);\n\n if (!info) {\n return null;\n }\n\n // Can only reuse if not currently attached elsewhere\n if (info.isAttached) {\n return null;\n }\n\n return info;\n }\n\n /**\n * Store a component in the cache and mark as attached.\n */\n cacheComponent(\n componentRef: ComponentRef<BaseResourceComponent>,\n wrapperElement: HTMLElement,\n resourceData: ResourceData,\n tabId: string\n ): void {\n // Use driverClass (the actual component class name) as the resourceType for the cache key,\n // NOT resourceData.ResourceType (which is often just \"Custom\" for dashboard resources).\n // This must match the lookup key used in getCachedComponent/markAsAttached/markAsDetached.\n const resolvedResourceType = resourceData.Configuration?.resourceTypeDriverClass\n || resourceData.Configuration?.driverClass\n || resourceData.ResourceType;\n // Entity name discriminates between distinct \"new record\" tabs (empty recordId).\n // For non-Records resource types this is undefined, leaving the key unchanged.\n const discriminator = resourceData.Configuration?.Entity as string | undefined;\n const key = this.getCacheKey(\n resolvedResourceType,\n resourceData.ResourceRecordID || '',\n resourceData.Configuration?.applicationId || '',\n discriminator\n );\n\n const info: CachedComponentInfo = {\n componentRef,\n wrapperElement,\n resourceType: resolvedResourceType,\n resourceRecordId: resourceData.ResourceRecordID || '',\n applicationId: resourceData.Configuration?.applicationId || '',\n keyDiscriminator: discriminator,\n isAttached: true,\n attachedToTabId: tabId,\n lastUsed: new Date(),\n createdAt: new Date(),\n resourceData\n };\n\n this.cache.set(key, info);\n }\n\n /**\n * Mark a component as attached. Lookup by resource identity.\n */\n markAsAttached(resourceType: string, recordId: string, appId: string, tabId: string, discriminator?: string): void {\n const key = this.getCacheKey(resourceType, recordId, appId, discriminator);\n const info = this.cache.get(key);\n\n if (info) {\n info.isAttached = true;\n info.attachedToTabId = tabId;\n info.lastUsed = new Date();\n }\n }\n\n /**\n * Mark a component as detached (available for reuse). Lookup by resource identity.\n *\n * This is the ONLY way to detach a component. Both single-resource mode and\n * Golden Layout mode use this same method to ensure consistent cache behavior.\n */\n markAsDetached(resourceType: string, recordId: string, appId: string, discriminator?: string): CachedComponentInfo | null {\n const key = this.getCacheKey(resourceType, recordId, appId, discriminator);\n const info = this.cache.get(key);\n if (!info) return null;\n\n info.isAttached = false;\n info.attachedToTabId = null;\n info.lastUsed = new Date();\n this.EvictIfNeeded();\n return info;\n }\n\n /**\n * Re-key a cached component when its underlying record identity changes.\n *\n * Used when a \"new record\" component (cached at `recordId = ''`) becomes a saved record\n * (now identified by its new PK). Without re-keying, the next \"new record\" request would\n * still resolve to this same cache entry, surfacing the previously-saved record on a form\n * the user expects to be blank.\n *\n * Preserves the live component instance — only the cache key + stored identity change.\n * Returns true if a matching entry was found and re-keyed, false otherwise.\n */\n rekeyComponent(\n resourceType: string,\n oldRecordId: string,\n newRecordId: string,\n appId: string,\n oldDiscriminator?: string,\n newDiscriminator?: string\n ): boolean {\n const oldKey = this.getCacheKey(resourceType, oldRecordId, appId, oldDiscriminator);\n const info = this.cache.get(oldKey);\n if (!info) {\n return false;\n }\n\n // Once a record has a real id, the discriminator no longer matters (the id is unique).\n // Default the new discriminator to undefined unless explicitly provided.\n const newKey = this.getCacheKey(resourceType, newRecordId, appId, newDiscriminator);\n if (oldKey === newKey) {\n return false;\n }\n\n info.resourceRecordId = newRecordId;\n info.keyDiscriminator = newDiscriminator;\n info.lastUsed = new Date();\n if (info.resourceData) {\n info.resourceData.ResourceRecordID = newRecordId;\n }\n\n this.cache.delete(oldKey);\n this.cache.set(newKey, info);\n return true;\n }\n\n /**\n * Find a cached component by tab ID and detach it.\n * This is a convenience wrapper for callers that only know the tab ID\n * (e.g., Golden Layout tab close events). It resolves the tab ID to\n * resource identity, then delegates to the identity-based markAsDetached.\n */\n findAndDetachByTabId(tabId: string): CachedComponentInfo | null {\n const entry = Array.from(this.cache.entries())\n .find(([_, info]) => info.attachedToTabId === tabId);\n\n if (!entry) return null;\n\n const [_, info] = entry;\n return this.markAsDetached(info.resourceType, info.resourceRecordId, info.applicationId, info.keyDiscriminator);\n }\n\n /**\n * Evict least-recently-used detached components when over the limit.\n * Only evicts components that are not currently attached.\n */\n private EvictIfNeeded(): void {\n if (ComponentCacheManager.MaxDetachedComponents <= 0) return;\n\n const detached = Array.from(this.cache.entries())\n .filter(([_, info]) => !info.isAttached)\n .sort((a, b) => a[1].lastUsed.getTime() - b[1].lastUsed.getTime());\n\n while (detached.length > ComponentCacheManager.MaxDetachedComponents) {\n const [key, info] = detached.shift()!;\n this.appRef.detachView(info.componentRef.hostView);\n info.componentRef.destroy();\n this.cache.delete(key);\n }\n }\n\n /**\n * Get component info by tab ID (for finding what's attached to a tab).\n * Uses linear scan since tabId is metadata, not a key.\n */\n getComponentByTabId(tabId: string): CachedComponentInfo | null {\n const entry = Array.from(this.cache.entries())\n .find(([_, info]) => info.attachedToTabId === tabId);\n\n return entry ? entry[1] : null;\n }\n\n /**\n * Remove and destroy a specific component from cache by resource identity.\n */\n destroyComponent(resourceType: string, recordId: string, appId: string, discriminator?: string): void {\n const key = this.getCacheKey(resourceType, recordId, appId, discriminator);\n const info = this.cache.get(key);\n\n if (!info) return;\n\n this.appRef.detachView(info.componentRef.hostView);\n info.componentRef.destroy();\n this.cache.delete(key);\n }\n\n /**\n * Remove and destroy component by tab ID (convenience for Golden Layout tab close).\n */\n destroyComponentByTabId(tabId: string): void {\n const entry = Array.from(this.cache.entries())\n .find(([_, info]) => info.attachedToTabId === tabId);\n\n if (!entry) return;\n\n const [key, info] = entry;\n this.appRef.detachView(info.componentRef.hostView);\n info.componentRef.destroy();\n this.cache.delete(key);\n }\n\n /**\n * Clear the entire cache, destroying all components.\n * Call this on user logout or app shutdown.\n */\n clearCache(): void {\n this.cache.forEach(info => {\n this.appRef.detachView(info.componentRef.hostView);\n info.componentRef.destroy();\n });\n this.cache.clear();\n }\n\n /**\n * Selectively clear cached components matching a predicate.\n * Components that match are destroyed; those that don't are kept.\n *\n * Use this for tenant switching: clear org-scoped components while\n * keeping system/global components alive.\n *\n * @param predicate Return true for components that should be destroyed.\n * @returns Number of components destroyed.\n */\n ClearCacheByPredicate(predicate: (info: CachedComponentInfo) => boolean): number {\n let destroyed = 0;\n const toRemove: string[] = [];\n\n this.cache.forEach((info, key) => {\n if (predicate(info)) {\n this.appRef.detachView(info.componentRef.hostView);\n info.componentRef.destroy();\n toRemove.push(key);\n destroyed++;\n }\n });\n\n for (const key of toRemove) {\n this.cache.delete(key);\n }\n\n return destroyed;\n }\n\n /**\n * Get cache statistics for debugging.\n */\n getCacheStats(): {\n total: number;\n attached: number;\n detached: number;\n byResourceType: Map<string, number>;\n } {\n const stats = {\n total: this.cache.size,\n attached: 0,\n detached: 0,\n byResourceType: new Map<string, number>()\n };\n\n this.cache.forEach(info => {\n if (info.isAttached) {\n stats.attached++;\n } else {\n stats.detached++;\n }\n\n const count = stats.byResourceType.get(info.resourceType) || 0;\n stats.byResourceType.set(info.resourceType, count + 1);\n });\n\n return stats;\n }\n}\n"]}
|
|
@@ -136,6 +136,48 @@ export declare class TabContainerComponent extends BaseAngularComponent implemen
|
|
|
136
136
|
*/
|
|
137
137
|
private saveCurrentComponentQueryParams;
|
|
138
138
|
private cleanupSingleResourceComponent;
|
|
139
|
+
/**
|
|
140
|
+
* Handle a record-saved event from a hosted resource component.
|
|
141
|
+
*
|
|
142
|
+
* Does two things on every save:
|
|
143
|
+
*
|
|
144
|
+
* 1. **Re-key new-record tabs.** When a tab opened as "Create New Record" (empty
|
|
145
|
+
* `resourceRecordId`) transitions to a saved record, we update both the workspace
|
|
146
|
+
* tab's id and the component-cache key to the new PK. Without this, the next
|
|
147
|
+
* `OpenNewEntityRecord(<sameEntity>)` would match this tab (both have empty
|
|
148
|
+
* `resourceRecordId`) and focus the stale form instead of opening a fresh one.
|
|
149
|
+
* For existing records being re-saved, this step is a no-op.
|
|
150
|
+
*
|
|
151
|
+
* 2. **Refresh the tab title.** Whether new or existing, the entity's display name
|
|
152
|
+
* (typically its `Name` field) may have changed. We call the resource component's
|
|
153
|
+
* `GetResourceDisplayName` and push the result to the tab — so a tab that read
|
|
154
|
+
* "New Foo Record" before save now reads the actual entered name.
|
|
155
|
+
*/
|
|
156
|
+
private handleResourceRecordSaved;
|
|
157
|
+
/**
|
|
158
|
+
* After a save, invalidate the entity-record-name cache for the saved record and then
|
|
159
|
+
* push the resource component's current display name into the tab title.
|
|
160
|
+
*
|
|
161
|
+
* Errors are swallowed (logged via the inner call paths) — failing to refresh a tab
|
|
162
|
+
* title should never break the save flow.
|
|
163
|
+
*/
|
|
164
|
+
private refreshTabTitleAfterSave;
|
|
165
|
+
/**
|
|
166
|
+
* A resource component asked to be dismissed — typically the user clicked Discard
|
|
167
|
+
* on a brand-new record. Behavior depends on workspace state:
|
|
168
|
+
*
|
|
169
|
+
* - **Multi-tab mode (or > 1 tab)**: just close the tab. `WorkspaceStateManager.CloseTab`
|
|
170
|
+
* activates the next tab, `syncTabsWithConfiguration` removes the tab from Golden
|
|
171
|
+
* Layout, and the user lands on whatever tab was previously active.
|
|
172
|
+
*
|
|
173
|
+
* - **Last tab in the workspace**: `CloseTab` intentionally keeps the last tab around
|
|
174
|
+
* (just unpins it) so the workspace is never empty — correct for the
|
|
175
|
+
* user-clicked-X-button case, wrong here. We want the user OFF this discarded form.
|
|
176
|
+
* Workaround: `CloseTab` makes the tab unpinned (= a temp tab), then we open the
|
|
177
|
+
* app's default tab via `CreateDefaultTab()` which goes through `OpenTab` and
|
|
178
|
+
* replaces the now-temp tab. End result: user lands on the app's home/default view.
|
|
179
|
+
*/
|
|
180
|
+
private handleResourceCloseRequested;
|
|
139
181
|
/**
|
|
140
182
|
* Generate a signature for tab content to detect when content changes
|
|
141
183
|
* This is needed because in single-resource mode, the same tab ID can have different content
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tab-container.component.d.ts","sourceRoot":"","sources":["../../../../../src/lib/shell/components/tabs/tab-container.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,MAAM,EACN,SAAS,EACT,aAAa,EAEb,UAAU,EACV,cAAc,EACd,mBAAmB,EAKnB,iBAAiB,EAGjB,YAAY,EAEb,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAKnB,MAAM,qCAAqC,CAAC;AAM7C,OAAO,EAAyB,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAEvF,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;;AACrE;;;;;;;;;GASG;AACH,qBAOa,qBAAsB,SAAQ,oBAAqB,YAAW,MAAM,EAAE,SAAS,EAAE,aAAa;IAoDvG,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,gBAAgB;IACxB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,mBAAmB;IAC3B,OAAO,CAAC,GAAG;IAxDgC,WAAW,EAAG,UAAU,CAAC,cAAc,CAAC,CAAC;IAC9B,sBAAsB,EAAG,UAAU,CAAC,cAAc,CAAC,CAAC;IAE5G;;;;OAIG;IACO,yBAAyB,qBAA4B;IAE/D;;;OAGG;IACO,eAAe,qBAA4B;IAErD,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,oBAAoB,CAAK;IACjC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAK;IAC7C,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,yBAAyB,CAAS;IAG1C,OAAO,CAAC,aAAa,CAA0D;IAK/E,OAAO,CAAC,oBAAoB,CAAqB;IAGjD,OAAO,CAAC,YAAY,CAAwB;IAI5C,qBAAqB,UAAS;IAC9B,OAAO,CAAC,0BAA0B,CAAoD;IACtF,6EAA6E;IAC7E,OAAO,CAAC,2BAA2B,
|
|
1
|
+
{"version":3,"file":"tab-container.component.d.ts","sourceRoot":"","sources":["../../../../../src/lib/shell/components/tabs/tab-container.component.ts"],"names":[],"mappings":"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"}
|
|
@@ -480,6 +480,9 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
480
480
|
// Get the active tab (or first tab)
|
|
481
481
|
const activeTab = config.tabs.find(t => t.id === config.activeTabId) || config.tabs[0];
|
|
482
482
|
if (!activeTab) {
|
|
483
|
+
// Config has tabs but none match activeTabId and the array fallback failed.
|
|
484
|
+
// This shouldn't happen, but if it does, unblock the loading screen.
|
|
485
|
+
this.emitFirstLoadCompleteOnce();
|
|
483
486
|
return;
|
|
484
487
|
}
|
|
485
488
|
// Track which content we're loading (signature includes resource type and record ID)
|
|
@@ -500,25 +503,31 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
500
503
|
const resourceData = await this.getResourceDataFromTab(activeTab);
|
|
501
504
|
if (!resourceData) {
|
|
502
505
|
LogError(`Unable to create ResourceData for tab: ${activeTab.title}`);
|
|
506
|
+
// Unblock the shell's loading overlay — stale or malformed tab config shouldn't
|
|
507
|
+
// leave the user stuck on the loading screen forever
|
|
508
|
+
this.emitFirstLoadCompleteOnce();
|
|
503
509
|
return;
|
|
504
510
|
}
|
|
505
511
|
// Get driver class for component lookup
|
|
506
512
|
const driverClass = resourceData.Configuration?.resourceTypeDriverClass || resourceData.ResourceType;
|
|
513
|
+
// Entity name discriminates between "new record" tabs of different entities (all
|
|
514
|
+
// have empty ResourceRecordID otherwise). For non-Records resources this is undefined.
|
|
515
|
+
const cacheDiscriminator = resourceData.Configuration?.Entity;
|
|
507
516
|
// **OPTIMIZATION: Check cache first to reuse existing loaded component**
|
|
508
|
-
const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId);
|
|
517
|
+
const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId, cacheDiscriminator);
|
|
509
518
|
if (cached) {
|
|
510
519
|
// Clean up previous single-resource component (if different)
|
|
511
520
|
this.cleanupSingleResourceComponent();
|
|
512
521
|
// Mark cached component as attached to this tab (it was detached / available for reuse).
|
|
513
522
|
// IMPORTANT: We use markAsAttached here, NOT markAsDetached — the component is being
|
|
514
523
|
// reattached to the DOM and should NOT be eligible for LRU eviction.
|
|
515
|
-
this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId, activeTab.id);
|
|
524
|
+
this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId, activeTab.id, cacheDiscriminator);
|
|
516
525
|
// Reattach the cached wrapper element to single-resource container
|
|
517
526
|
cached.wrapperElement.style.height = "100%"; // Ensure full height
|
|
518
527
|
container.appendChild(cached.wrapperElement);
|
|
519
528
|
// Store reference and identity for cleanup/detachment
|
|
520
529
|
this.singleResourceComponentRef = cached.componentRef;
|
|
521
|
-
this.singleResourceCacheIdentity = { driverClass, recordId: resourceData.ResourceRecordID || '', appId: activeTab.applicationId, tabId: activeTab.id };
|
|
530
|
+
this.singleResourceCacheIdentity = { driverClass, recordId: resourceData.ResourceRecordID || '', appId: activeTab.applicationId, tabId: activeTab.id, discriminator: cacheDiscriminator };
|
|
522
531
|
// Reconcile the cached component's preserved queryParams with any INCOMING navigation
|
|
523
532
|
// intent already on the tab config (e.g. a Home pin / deep link that targeted a specific
|
|
524
533
|
// conversation via SwitchToApp before we got here).
|
|
@@ -592,6 +601,17 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
592
601
|
}
|
|
593
602
|
}
|
|
594
603
|
};
|
|
604
|
+
// When a record is saved, re-key the tab and cache (new-record → saved transition)
|
|
605
|
+
// and refresh the tab title to reflect the entity's current display name.
|
|
606
|
+
instance.ResourceRecordSavedEvent = (entity) => {
|
|
607
|
+
this.handleResourceRecordSaved(driverClass, activeTab.applicationId, activeTab.id, instance, entity);
|
|
608
|
+
};
|
|
609
|
+
// Resource asked to be closed (e.g., user discarded a brand-new record — there's
|
|
610
|
+
// no actual record to view, so leaving the tab open serves no purpose and would
|
|
611
|
+
// also poison the cache for the next "Create New" click of the same entity).
|
|
612
|
+
instance.ResourceCloseRequestedEvent = () => {
|
|
613
|
+
this.handleResourceCloseRequested(activeTab.id, instance);
|
|
614
|
+
};
|
|
595
615
|
// Get the native element and append to container
|
|
596
616
|
const nativeElement = componentRef.hostView.rootNodes[0];
|
|
597
617
|
container.appendChild(nativeElement);
|
|
@@ -605,7 +625,7 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
605
625
|
this.cacheManager.cacheComponent(componentRef, wrapperElement, resourceData, activeTab.id);
|
|
606
626
|
// Store reference and identity for cleanup/detachment
|
|
607
627
|
this.singleResourceComponentRef = componentRef;
|
|
608
|
-
this.singleResourceCacheIdentity = { driverClass, recordId: resourceData.ResourceRecordID || '', appId: activeTab.applicationId, tabId: activeTab.id };
|
|
628
|
+
this.singleResourceCacheIdentity = { driverClass, recordId: resourceData.ResourceRecordID || '', appId: activeTab.applicationId, tabId: activeTab.id, discriminator: cacheDiscriminator };
|
|
609
629
|
}
|
|
610
630
|
/**
|
|
611
631
|
* Clean up single-resource mode component
|
|
@@ -654,9 +674,9 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
654
674
|
cleanupSingleResourceComponent() {
|
|
655
675
|
if (this.singleResourceComponentRef) {
|
|
656
676
|
if (this.singleResourceCacheIdentity) {
|
|
657
|
-
const { driverClass, recordId, appId } = this.singleResourceCacheIdentity;
|
|
677
|
+
const { driverClass, recordId, appId, discriminator } = this.singleResourceCacheIdentity;
|
|
658
678
|
// Mark as DETACHED by resource identity — the ONE consistent key used everywhere.
|
|
659
|
-
this.cacheManager.markAsDetached(driverClass, recordId, appId);
|
|
679
|
+
this.cacheManager.markAsDetached(driverClass, recordId, appId, discriminator);
|
|
660
680
|
}
|
|
661
681
|
this.singleResourceComponentRef = null;
|
|
662
682
|
this.singleResourceCacheIdentity = null;
|
|
@@ -671,6 +691,162 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
671
691
|
}
|
|
672
692
|
}
|
|
673
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Handle a record-saved event from a hosted resource component.
|
|
696
|
+
*
|
|
697
|
+
* Does two things on every save:
|
|
698
|
+
*
|
|
699
|
+
* 1. **Re-key new-record tabs.** When a tab opened as "Create New Record" (empty
|
|
700
|
+
* `resourceRecordId`) transitions to a saved record, we update both the workspace
|
|
701
|
+
* tab's id and the component-cache key to the new PK. Without this, the next
|
|
702
|
+
* `OpenNewEntityRecord(<sameEntity>)` would match this tab (both have empty
|
|
703
|
+
* `resourceRecordId`) and focus the stale form instead of opening a fresh one.
|
|
704
|
+
* For existing records being re-saved, this step is a no-op.
|
|
705
|
+
*
|
|
706
|
+
* 2. **Refresh the tab title.** Whether new or existing, the entity's display name
|
|
707
|
+
* (typically its `Name` field) may have changed. We call the resource component's
|
|
708
|
+
* `GetResourceDisplayName` and push the result to the tab — so a tab that read
|
|
709
|
+
* "New Foo Record" before save now reads the actual entered name.
|
|
710
|
+
*/
|
|
711
|
+
handleResourceRecordSaved(driverClass, appId, tabId, instance, entity) {
|
|
712
|
+
const tab = this.workspaceManager.GetTab(tabId);
|
|
713
|
+
if (!tab) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const oldRecordId = tab.resourceRecordId || '';
|
|
717
|
+
// ToURLSegment, NOT ToString — must match the format NavigationService.OpenEntityRecord
|
|
718
|
+
// uses and that EntityRecordResource.GetPrimaryKey expects via LoadFromURLSegment.
|
|
719
|
+
const newRecordId = entity?.PrimaryKey?.ToURLSegment?.() ?? '';
|
|
720
|
+
const isNewRecordTransition = oldRecordId === '' && !!newRecordId;
|
|
721
|
+
// The cache entry for an empty-recordId tab was keyed with the entity name as
|
|
722
|
+
// discriminator (to avoid new-record collisions across entities). Re-key has to
|
|
723
|
+
// pass that same old discriminator to find the entry. After save the record has
|
|
724
|
+
// a real PK, so the new key needs no discriminator.
|
|
725
|
+
const oldDiscriminator = tab.configuration?.['Entity'];
|
|
726
|
+
if (isNewRecordTransition) {
|
|
727
|
+
// Re-key the cache entry first, so when the workspace config emission triggers
|
|
728
|
+
// signature/needsReload checks below, the cache lookup at the new id succeeds.
|
|
729
|
+
this.cacheManager.rekeyComponent(driverClass, oldRecordId, newRecordId, appId, oldDiscriminator, undefined);
|
|
730
|
+
// Keep the in-memory single-resource identity in sync so detach uses the correct key.
|
|
731
|
+
if (this.singleResourceCacheIdentity?.tabId === tabId) {
|
|
732
|
+
this.singleResourceCacheIdentity = {
|
|
733
|
+
...this.singleResourceCacheIdentity,
|
|
734
|
+
recordId: newRecordId,
|
|
735
|
+
discriminator: undefined
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
// Pre-compute and stamp the new signature so the configuration-subscription path
|
|
739
|
+
// in single-resource mode doesn't see a "content changed" delta and trigger a
|
|
740
|
+
// needless reload cycle (which would destroy the currently attached component).
|
|
741
|
+
if (this.useSingleResourceMode && this.currentSingleResourceSignature !== null) {
|
|
742
|
+
const updatedTab = {
|
|
743
|
+
...tab,
|
|
744
|
+
resourceRecordId: newRecordId,
|
|
745
|
+
configuration: { ...tab.configuration, recordId: newRecordId, isNew: undefined }
|
|
746
|
+
};
|
|
747
|
+
this.currentSingleResourceSignature = this.getTabContentSignature(updatedTab);
|
|
748
|
+
}
|
|
749
|
+
// Propagate the new id to the workspace tab.
|
|
750
|
+
this.workspaceManager.UpdateTabResourceRecordId(tabId, newRecordId);
|
|
751
|
+
}
|
|
752
|
+
// Always refresh the tab title — the entity's display-name field (typically Name)
|
|
753
|
+
// may have just been set or changed. Done after the rekey so the resource component's
|
|
754
|
+
// Data.ResourceRecordID reflects the saved PK before GetResourceDisplayName reads it.
|
|
755
|
+
//
|
|
756
|
+
// ProviderBase caches GetEntityRecordName results, so a plain call here would return
|
|
757
|
+
// the pre-edit name. Pre-warm the cache with a forceRefresh so the downstream read
|
|
758
|
+
// inside GetResourceDisplayName picks up the saved name. Only meaningful for entity
|
|
759
|
+
// records with a PK; other resource types' GetResourceDisplayName doesn't hit the
|
|
760
|
+
// record-name cache and will work either way.
|
|
761
|
+
void this.refreshTabTitleAfterSave(tabId, instance, entity);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* After a save, invalidate the entity-record-name cache for the saved record and then
|
|
765
|
+
* push the resource component's current display name into the tab title.
|
|
766
|
+
*
|
|
767
|
+
* Errors are swallowed (logged via the inner call paths) — failing to refresh a tab
|
|
768
|
+
* title should never break the save flow.
|
|
769
|
+
*/
|
|
770
|
+
async refreshTabTitleAfterSave(tabId, instance, entity) {
|
|
771
|
+
try {
|
|
772
|
+
const entityName = entity?.EntityInfo?.Name;
|
|
773
|
+
const pk = entity?.PrimaryKey;
|
|
774
|
+
if (entityName && pk?.HasValue) {
|
|
775
|
+
// forceRefresh: true overwrites the cached (pre-edit) name with the fresh one.
|
|
776
|
+
// GetResourceDisplayName (called next) reads the same cache and now sees fresh data.
|
|
777
|
+
const md = instance.ProviderToUse;
|
|
778
|
+
await md.GetEntityRecordName(entityName, pk, md.CurrentUser, true);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
// Cache pre-warm failures are non-fatal — the title update below will fall back
|
|
783
|
+
// to whatever the cache has and the user can still interact with the form.
|
|
784
|
+
}
|
|
785
|
+
await this.updateTabTitleFromResource(tabId, instance, instance.Data);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* A resource component asked to be dismissed — typically the user clicked Discard
|
|
789
|
+
* on a brand-new record. Behavior depends on workspace state:
|
|
790
|
+
*
|
|
791
|
+
* - **Multi-tab mode (or > 1 tab)**: just close the tab. `WorkspaceStateManager.CloseTab`
|
|
792
|
+
* activates the next tab, `syncTabsWithConfiguration` removes the tab from Golden
|
|
793
|
+
* Layout, and the user lands on whatever tab was previously active.
|
|
794
|
+
*
|
|
795
|
+
* - **Last tab in the workspace**: `CloseTab` intentionally keeps the last tab around
|
|
796
|
+
* (just unpins it) so the workspace is never empty — correct for the
|
|
797
|
+
* user-clicked-X-button case, wrong here. We want the user OFF this discarded form.
|
|
798
|
+
* Workaround: `CloseTab` makes the tab unpinned (= a temp tab), then we open the
|
|
799
|
+
* app's default tab via `CreateDefaultTab()` which goes through `OpenTab` and
|
|
800
|
+
* replaces the now-temp tab. End result: user lands on the app's home/default view.
|
|
801
|
+
*/
|
|
802
|
+
async handleResourceCloseRequested(tabId, _instance) {
|
|
803
|
+
const config = this.workspaceManager.GetConfiguration();
|
|
804
|
+
const tab = config?.tabs.find(t => t.id === tabId);
|
|
805
|
+
const isLastTab = config?.tabs.length === 1 && config.tabs[0].id === tabId;
|
|
806
|
+
// DESTROY (not just detach) the cached component before closing. The default
|
|
807
|
+
// tab-close path detaches and keeps components alive for reuse — but the
|
|
808
|
+
// discarded form is exactly what we DON'T want to keep: it holds a stale
|
|
809
|
+
// BaseEntity in view mode. The next "Create New Record" click for the same
|
|
810
|
+
// entity would otherwise hit the cache discriminator lookup, find this stale
|
|
811
|
+
// component, and reattach it — surfacing a blank form in view mode instead
|
|
812
|
+
// of a fresh edit-mode form. Destroy here forces a cache miss on the next click.
|
|
813
|
+
if (this.singleResourceCacheIdentity?.tabId === tabId) {
|
|
814
|
+
// Single-resource mode: the component is tracked via singleResource* fields,
|
|
815
|
+
// and its cache entry is currently marked attached. Use the identity directly
|
|
816
|
+
// to destroy it (markAsDetached would clear attachedToTabId and break a
|
|
817
|
+
// subsequent destroyComponentByTabId lookup).
|
|
818
|
+
const { driverClass, recordId, appId, discriminator } = this.singleResourceCacheIdentity;
|
|
819
|
+
this.cacheManager.destroyComponent(driverClass, recordId, appId, discriminator);
|
|
820
|
+
this.singleResourceComponentRef = null;
|
|
821
|
+
this.singleResourceCacheIdentity = null;
|
|
822
|
+
// Clear the host container's DOM so the user isn't briefly looking at the
|
|
823
|
+
// destroyed component's wrapper between CloseTab and the default-tab load.
|
|
824
|
+
const directContainer = this.directContentContainer?.nativeElement;
|
|
825
|
+
if (directContainer) {
|
|
826
|
+
while (directContainer.firstChild)
|
|
827
|
+
directContainer.removeChild(directContainer.firstChild);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
// Multi-tab mode: cache entry is keyed by attachedToTabId; the convenience
|
|
832
|
+
// method handles the lookup.
|
|
833
|
+
this.cacheManager.destroyComponentByTabId(tabId);
|
|
834
|
+
this.componentRefs.delete(tabId);
|
|
835
|
+
}
|
|
836
|
+
this.workspaceManager.CloseTab(tabId);
|
|
837
|
+
if (isLastTab && tab) {
|
|
838
|
+
// CloseTab kept the tab around but unpinned it. Replace it with the app's
|
|
839
|
+
// default tab so the user lands on a meaningful surface (typically the
|
|
840
|
+
// home dashboard) instead of staying on the discarded form.
|
|
841
|
+
const app = this.appManager.GetAppById(tab.applicationId);
|
|
842
|
+
if (app) {
|
|
843
|
+
const defaultTabRequest = await app.CreateDefaultTab();
|
|
844
|
+
if (defaultTabRequest) {
|
|
845
|
+
this.workspaceManager.OpenTab(defaultTabRequest, app.GetColor());
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
674
850
|
/**
|
|
675
851
|
* Generate a signature for tab content to detect when content changes
|
|
676
852
|
* This is needed because in single-resource mode, the same tab ID can have different content
|
|
@@ -735,31 +911,37 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
735
911
|
const tab = this.workspaceManager.GetTab(tabId);
|
|
736
912
|
if (!tab) {
|
|
737
913
|
LogError(`Tab not found: ${tabId}`);
|
|
914
|
+
this.emitFirstLoadCompleteOnce();
|
|
738
915
|
return;
|
|
739
916
|
}
|
|
740
917
|
// Get the container element from Golden Layout
|
|
741
918
|
const glContainer = container;
|
|
742
919
|
if (!glContainer?.element) {
|
|
743
920
|
LogError('Golden Layout container element not found');
|
|
921
|
+
this.emitFirstLoadCompleteOnce();
|
|
744
922
|
return;
|
|
745
923
|
}
|
|
746
924
|
// Extract resource data from tab configuration
|
|
747
925
|
const resourceData = await this.getResourceDataFromTab(tab);
|
|
748
926
|
if (!resourceData) {
|
|
749
927
|
LogError(`Unable to create ResourceData for tab: ${tab.title}`);
|
|
928
|
+
this.emitFirstLoadCompleteOnce();
|
|
750
929
|
return;
|
|
751
930
|
}
|
|
752
931
|
// Clear any existing content from the container (important for tab reuse)
|
|
753
932
|
glContainer.element.innerHTML = '';
|
|
754
933
|
// Get driver class for cache lookup (resolves to actual component class name)
|
|
755
934
|
const driverClass = resourceData.Configuration?.resourceTypeDriverClass || resourceData.ResourceType;
|
|
935
|
+
// Discriminate distinct "new record" tabs of different entities (all have empty
|
|
936
|
+
// ResourceRecordID otherwise — would silently collide in the cache).
|
|
937
|
+
const cacheDiscriminator = resourceData.Configuration?.Entity;
|
|
756
938
|
// Check if we have a cached component for this resource
|
|
757
|
-
const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', tab.applicationId);
|
|
939
|
+
const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', tab.applicationId, cacheDiscriminator);
|
|
758
940
|
if (cached) {
|
|
759
941
|
// Reattach the cached wrapper element
|
|
760
942
|
glContainer.element.appendChild(cached.wrapperElement);
|
|
761
943
|
// Mark as attached to this tab
|
|
762
|
-
this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', tab.applicationId, tabId);
|
|
944
|
+
this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', tab.applicationId, tabId, cacheDiscriminator);
|
|
763
945
|
// Keep legacy componentRefs map updated
|
|
764
946
|
this.componentRefs.set(tabId, cached.componentRef);
|
|
765
947
|
// If resource is already loaded, update tab title immediately and signal
|
|
@@ -802,10 +984,11 @@ export class TabContainerComponent extends BaseAngularComponent {
|
|
|
802
984
|
this.emitFirstLoadCompleteOnce();
|
|
803
985
|
};
|
|
804
986
|
instance.ResourceRecordSavedEvent = (entity) => {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
987
|
+
this.handleResourceRecordSaved(driverClass, tab.applicationId, tabId, instance, entity);
|
|
988
|
+
};
|
|
989
|
+
// Resource asked to be closed (e.g., user discarded a brand-new record).
|
|
990
|
+
instance.ResourceCloseRequestedEvent = () => {
|
|
991
|
+
this.handleResourceCloseRequested(tabId, instance);
|
|
809
992
|
};
|
|
810
993
|
// Wire up display name change notifications
|
|
811
994
|
instance.DisplayNameChangedEvent = (newName) => {
|