@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.
Files changed (31) hide show
  1. package/dist/generated/lazy-feature-config.d.ts +1 -1
  2. package/dist/generated/lazy-feature-config.d.ts.map +1 -1
  3. package/dist/generated/lazy-feature-config.js +4 -2
  4. package/dist/generated/lazy-feature-config.js.map +1 -1
  5. package/dist/lib/resource-wrappers/artifact-resource.component.d.ts +10 -0
  6. package/dist/lib/resource-wrappers/artifact-resource.component.d.ts.map +1 -1
  7. package/dist/lib/resource-wrappers/artifact-resource.component.js +15 -5
  8. package/dist/lib/resource-wrappers/artifact-resource.component.js.map +1 -1
  9. package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
  10. package/dist/lib/resource-wrappers/record-resource.component.js +22 -6
  11. package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
  12. package/dist/lib/shell/components/header/app-nav.component.d.ts.map +1 -1
  13. package/dist/lib/shell/components/header/app-nav.component.js +12 -0
  14. package/dist/lib/shell/components/header/app-nav.component.js.map +1 -1
  15. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +24 -5
  16. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
  17. package/dist/lib/shell/components/tabs/component-cache-manager.js +58 -14
  18. package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
  19. package/dist/lib/shell/components/tabs/tab-container.component.d.ts +42 -0
  20. package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
  21. package/dist/lib/shell/components/tabs/tab-container.component.js +195 -12
  22. package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
  23. package/dist/lib/shell/shell.component.d.ts +5 -4
  24. package/dist/lib/shell/shell.component.d.ts.map +1 -1
  25. package/dist/lib/shell/shell.component.js +20 -9
  26. package/dist/lib/shell/shell.component.js.map +1 -1
  27. package/dist/lib/single-record/single-record.component.d.ts +31 -37
  28. package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
  29. package/dist/lib/single-record/single-record.component.js +64 -306
  30. package/dist/lib/single-record/single-record.component.js.map +1 -1
  31. 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 || '__no_record__';
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
- const key = this.getCacheKey(resolvedResourceType, resourceData.ResourceRecordID || '', resourceData.Configuration?.applicationId || '');
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":"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"}
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
- // Update tab title if needed
806
- if (entity && entity.Get && entity.Get('Name')) {
807
- // TODO: Implement UpdateTabTitle in WorkspaceStateManager
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) => {