@memberjunction/ng-explorer-core 5.36.0 → 5.38.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 (51) 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/__tests__/form-resolver.service.test.d.ts +2 -0
  6. package/dist/lib/__tests__/form-resolver.service.test.d.ts.map +1 -0
  7. package/dist/lib/__tests__/form-resolver.service.test.js +258 -0
  8. package/dist/lib/__tests__/form-resolver.service.test.js.map +1 -0
  9. package/dist/lib/conversation-feedback/conversation-feedback.d.ts +108 -0
  10. package/dist/lib/conversation-feedback/conversation-feedback.d.ts.map +1 -0
  11. package/dist/lib/conversation-feedback/conversation-feedback.js +809 -0
  12. package/dist/lib/conversation-feedback/conversation-feedback.js.map +1 -0
  13. package/dist/lib/conversation-feedback/index.d.ts +2 -0
  14. package/dist/lib/conversation-feedback/index.d.ts.map +1 -0
  15. package/dist/lib/conversation-feedback/index.js +2 -0
  16. package/dist/lib/conversation-feedback/index.js.map +1 -0
  17. package/dist/lib/resource-wrappers/artifact-resource.component.d.ts +10 -0
  18. package/dist/lib/resource-wrappers/artifact-resource.component.d.ts.map +1 -1
  19. package/dist/lib/resource-wrappers/artifact-resource.component.js +15 -5
  20. package/dist/lib/resource-wrappers/artifact-resource.component.js.map +1 -1
  21. package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
  22. package/dist/lib/resource-wrappers/record-resource.component.js +22 -6
  23. package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
  24. package/dist/lib/services/form-resolver.service.d.ts +139 -0
  25. package/dist/lib/services/form-resolver.service.d.ts.map +1 -0
  26. package/dist/lib/services/form-resolver.service.js +235 -0
  27. package/dist/lib/services/form-resolver.service.js.map +1 -0
  28. package/dist/lib/shell/components/header/app-nav.component.d.ts.map +1 -1
  29. package/dist/lib/shell/components/header/app-nav.component.js +12 -0
  30. package/dist/lib/shell/components/header/app-nav.component.js.map +1 -1
  31. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +24 -5
  32. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
  33. package/dist/lib/shell/components/tabs/component-cache-manager.js +58 -14
  34. package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
  35. package/dist/lib/shell/components/tabs/tab-container.component.d.ts +42 -0
  36. package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
  37. package/dist/lib/shell/components/tabs/tab-container.component.js +186 -12
  38. package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
  39. package/dist/lib/single-record/single-record.component.d.ts +41 -3
  40. package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
  41. package/dist/lib/single-record/single-record.component.js +192 -23
  42. package/dist/lib/single-record/single-record.component.js.map +1 -1
  43. package/dist/module.d.ts +32 -30
  44. package/dist/module.d.ts.map +1 -1
  45. package/dist/module.js +15 -6
  46. package/dist/module.js.map +1 -1
  47. package/dist/public-api.d.ts +1 -0
  48. package/dist/public-api.d.ts.map +1 -1
  49. package/dist/public-api.js +1 -0
  50. package/dist/public-api.js.map +1 -1
  51. package/package.json +46 -44
@@ -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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"}
@@ -504,21 +504,24 @@ export class TabContainerComponent extends BaseAngularComponent {
504
504
  }
505
505
  // Get driver class for component lookup
506
506
  const driverClass = resourceData.Configuration?.resourceTypeDriverClass || resourceData.ResourceType;
507
+ // Entity name discriminates between "new record" tabs of different entities (all
508
+ // have empty ResourceRecordID otherwise). For non-Records resources this is undefined.
509
+ const cacheDiscriminator = resourceData.Configuration?.Entity;
507
510
  // **OPTIMIZATION: Check cache first to reuse existing loaded component**
508
- const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId);
511
+ const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId, cacheDiscriminator);
509
512
  if (cached) {
510
513
  // Clean up previous single-resource component (if different)
511
514
  this.cleanupSingleResourceComponent();
512
515
  // Mark cached component as attached to this tab (it was detached / available for reuse).
513
516
  // IMPORTANT: We use markAsAttached here, NOT markAsDetached — the component is being
514
517
  // reattached to the DOM and should NOT be eligible for LRU eviction.
515
- this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId, activeTab.id);
518
+ this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId, activeTab.id, cacheDiscriminator);
516
519
  // Reattach the cached wrapper element to single-resource container
517
520
  cached.wrapperElement.style.height = "100%"; // Ensure full height
518
521
  container.appendChild(cached.wrapperElement);
519
522
  // Store reference and identity for cleanup/detachment
520
523
  this.singleResourceComponentRef = cached.componentRef;
521
- this.singleResourceCacheIdentity = { driverClass, recordId: resourceData.ResourceRecordID || '', appId: activeTab.applicationId, tabId: activeTab.id };
524
+ this.singleResourceCacheIdentity = { driverClass, recordId: resourceData.ResourceRecordID || '', appId: activeTab.applicationId, tabId: activeTab.id, discriminator: cacheDiscriminator };
522
525
  // Reconcile the cached component's preserved queryParams with any INCOMING navigation
523
526
  // intent already on the tab config (e.g. a Home pin / deep link that targeted a specific
524
527
  // conversation via SwitchToApp before we got here).
@@ -592,6 +595,17 @@ export class TabContainerComponent extends BaseAngularComponent {
592
595
  }
593
596
  }
594
597
  };
598
+ // When a record is saved, re-key the tab and cache (new-record → saved transition)
599
+ // and refresh the tab title to reflect the entity's current display name.
600
+ instance.ResourceRecordSavedEvent = (entity) => {
601
+ this.handleResourceRecordSaved(driverClass, activeTab.applicationId, activeTab.id, instance, entity);
602
+ };
603
+ // Resource asked to be closed (e.g., user discarded a brand-new record — there's
604
+ // no actual record to view, so leaving the tab open serves no purpose and would
605
+ // also poison the cache for the next "Create New" click of the same entity).
606
+ instance.ResourceCloseRequestedEvent = () => {
607
+ this.handleResourceCloseRequested(activeTab.id, instance);
608
+ };
595
609
  // Get the native element and append to container
596
610
  const nativeElement = componentRef.hostView.rootNodes[0];
597
611
  container.appendChild(nativeElement);
@@ -605,7 +619,7 @@ export class TabContainerComponent extends BaseAngularComponent {
605
619
  this.cacheManager.cacheComponent(componentRef, wrapperElement, resourceData, activeTab.id);
606
620
  // Store reference and identity for cleanup/detachment
607
621
  this.singleResourceComponentRef = componentRef;
608
- this.singleResourceCacheIdentity = { driverClass, recordId: resourceData.ResourceRecordID || '', appId: activeTab.applicationId, tabId: activeTab.id };
622
+ this.singleResourceCacheIdentity = { driverClass, recordId: resourceData.ResourceRecordID || '', appId: activeTab.applicationId, tabId: activeTab.id, discriminator: cacheDiscriminator };
609
623
  }
610
624
  /**
611
625
  * Clean up single-resource mode component
@@ -654,9 +668,9 @@ export class TabContainerComponent extends BaseAngularComponent {
654
668
  cleanupSingleResourceComponent() {
655
669
  if (this.singleResourceComponentRef) {
656
670
  if (this.singleResourceCacheIdentity) {
657
- const { driverClass, recordId, appId } = this.singleResourceCacheIdentity;
671
+ const { driverClass, recordId, appId, discriminator } = this.singleResourceCacheIdentity;
658
672
  // Mark as DETACHED by resource identity — the ONE consistent key used everywhere.
659
- this.cacheManager.markAsDetached(driverClass, recordId, appId);
673
+ this.cacheManager.markAsDetached(driverClass, recordId, appId, discriminator);
660
674
  }
661
675
  this.singleResourceComponentRef = null;
662
676
  this.singleResourceCacheIdentity = null;
@@ -671,6 +685,162 @@ export class TabContainerComponent extends BaseAngularComponent {
671
685
  }
672
686
  }
673
687
  }
688
+ /**
689
+ * Handle a record-saved event from a hosted resource component.
690
+ *
691
+ * Does two things on every save:
692
+ *
693
+ * 1. **Re-key new-record tabs.** When a tab opened as "Create New Record" (empty
694
+ * `resourceRecordId`) transitions to a saved record, we update both the workspace
695
+ * tab's id and the component-cache key to the new PK. Without this, the next
696
+ * `OpenNewEntityRecord(<sameEntity>)` would match this tab (both have empty
697
+ * `resourceRecordId`) and focus the stale form instead of opening a fresh one.
698
+ * For existing records being re-saved, this step is a no-op.
699
+ *
700
+ * 2. **Refresh the tab title.** Whether new or existing, the entity's display name
701
+ * (typically its `Name` field) may have changed. We call the resource component's
702
+ * `GetResourceDisplayName` and push the result to the tab — so a tab that read
703
+ * "New Foo Record" before save now reads the actual entered name.
704
+ */
705
+ handleResourceRecordSaved(driverClass, appId, tabId, instance, entity) {
706
+ const tab = this.workspaceManager.GetTab(tabId);
707
+ if (!tab) {
708
+ return;
709
+ }
710
+ const oldRecordId = tab.resourceRecordId || '';
711
+ // ToURLSegment, NOT ToString — must match the format NavigationService.OpenEntityRecord
712
+ // uses and that EntityRecordResource.GetPrimaryKey expects via LoadFromURLSegment.
713
+ const newRecordId = entity?.PrimaryKey?.ToURLSegment?.() ?? '';
714
+ const isNewRecordTransition = oldRecordId === '' && !!newRecordId;
715
+ // The cache entry for an empty-recordId tab was keyed with the entity name as
716
+ // discriminator (to avoid new-record collisions across entities). Re-key has to
717
+ // pass that same old discriminator to find the entry. After save the record has
718
+ // a real PK, so the new key needs no discriminator.
719
+ const oldDiscriminator = tab.configuration?.['Entity'];
720
+ if (isNewRecordTransition) {
721
+ // Re-key the cache entry first, so when the workspace config emission triggers
722
+ // signature/needsReload checks below, the cache lookup at the new id succeeds.
723
+ this.cacheManager.rekeyComponent(driverClass, oldRecordId, newRecordId, appId, oldDiscriminator, undefined);
724
+ // Keep the in-memory single-resource identity in sync so detach uses the correct key.
725
+ if (this.singleResourceCacheIdentity?.tabId === tabId) {
726
+ this.singleResourceCacheIdentity = {
727
+ ...this.singleResourceCacheIdentity,
728
+ recordId: newRecordId,
729
+ discriminator: undefined
730
+ };
731
+ }
732
+ // Pre-compute and stamp the new signature so the configuration-subscription path
733
+ // in single-resource mode doesn't see a "content changed" delta and trigger a
734
+ // needless reload cycle (which would destroy the currently attached component).
735
+ if (this.useSingleResourceMode && this.currentSingleResourceSignature !== null) {
736
+ const updatedTab = {
737
+ ...tab,
738
+ resourceRecordId: newRecordId,
739
+ configuration: { ...tab.configuration, recordId: newRecordId, isNew: undefined }
740
+ };
741
+ this.currentSingleResourceSignature = this.getTabContentSignature(updatedTab);
742
+ }
743
+ // Propagate the new id to the workspace tab.
744
+ this.workspaceManager.UpdateTabResourceRecordId(tabId, newRecordId);
745
+ }
746
+ // Always refresh the tab title — the entity's display-name field (typically Name)
747
+ // may have just been set or changed. Done after the rekey so the resource component's
748
+ // Data.ResourceRecordID reflects the saved PK before GetResourceDisplayName reads it.
749
+ //
750
+ // ProviderBase caches GetEntityRecordName results, so a plain call here would return
751
+ // the pre-edit name. Pre-warm the cache with a forceRefresh so the downstream read
752
+ // inside GetResourceDisplayName picks up the saved name. Only meaningful for entity
753
+ // records with a PK; other resource types' GetResourceDisplayName doesn't hit the
754
+ // record-name cache and will work either way.
755
+ void this.refreshTabTitleAfterSave(tabId, instance, entity);
756
+ }
757
+ /**
758
+ * After a save, invalidate the entity-record-name cache for the saved record and then
759
+ * push the resource component's current display name into the tab title.
760
+ *
761
+ * Errors are swallowed (logged via the inner call paths) — failing to refresh a tab
762
+ * title should never break the save flow.
763
+ */
764
+ async refreshTabTitleAfterSave(tabId, instance, entity) {
765
+ try {
766
+ const entityName = entity?.EntityInfo?.Name;
767
+ const pk = entity?.PrimaryKey;
768
+ if (entityName && pk?.HasValue) {
769
+ // forceRefresh: true overwrites the cached (pre-edit) name with the fresh one.
770
+ // GetResourceDisplayName (called next) reads the same cache and now sees fresh data.
771
+ const md = instance.ProviderToUse;
772
+ await md.GetEntityRecordName(entityName, pk, md.CurrentUser, true);
773
+ }
774
+ }
775
+ catch {
776
+ // Cache pre-warm failures are non-fatal — the title update below will fall back
777
+ // to whatever the cache has and the user can still interact with the form.
778
+ }
779
+ await this.updateTabTitleFromResource(tabId, instance, instance.Data);
780
+ }
781
+ /**
782
+ * A resource component asked to be dismissed — typically the user clicked Discard
783
+ * on a brand-new record. Behavior depends on workspace state:
784
+ *
785
+ * - **Multi-tab mode (or > 1 tab)**: just close the tab. `WorkspaceStateManager.CloseTab`
786
+ * activates the next tab, `syncTabsWithConfiguration` removes the tab from Golden
787
+ * Layout, and the user lands on whatever tab was previously active.
788
+ *
789
+ * - **Last tab in the workspace**: `CloseTab` intentionally keeps the last tab around
790
+ * (just unpins it) so the workspace is never empty — correct for the
791
+ * user-clicked-X-button case, wrong here. We want the user OFF this discarded form.
792
+ * Workaround: `CloseTab` makes the tab unpinned (= a temp tab), then we open the
793
+ * app's default tab via `CreateDefaultTab()` which goes through `OpenTab` and
794
+ * replaces the now-temp tab. End result: user lands on the app's home/default view.
795
+ */
796
+ async handleResourceCloseRequested(tabId, _instance) {
797
+ const config = this.workspaceManager.GetConfiguration();
798
+ const tab = config?.tabs.find(t => t.id === tabId);
799
+ const isLastTab = config?.tabs.length === 1 && config.tabs[0].id === tabId;
800
+ // DESTROY (not just detach) the cached component before closing. The default
801
+ // tab-close path detaches and keeps components alive for reuse — but the
802
+ // discarded form is exactly what we DON'T want to keep: it holds a stale
803
+ // BaseEntity in view mode. The next "Create New Record" click for the same
804
+ // entity would otherwise hit the cache discriminator lookup, find this stale
805
+ // component, and reattach it — surfacing a blank form in view mode instead
806
+ // of a fresh edit-mode form. Destroy here forces a cache miss on the next click.
807
+ if (this.singleResourceCacheIdentity?.tabId === tabId) {
808
+ // Single-resource mode: the component is tracked via singleResource* fields,
809
+ // and its cache entry is currently marked attached. Use the identity directly
810
+ // to destroy it (markAsDetached would clear attachedToTabId and break a
811
+ // subsequent destroyComponentByTabId lookup).
812
+ const { driverClass, recordId, appId, discriminator } = this.singleResourceCacheIdentity;
813
+ this.cacheManager.destroyComponent(driverClass, recordId, appId, discriminator);
814
+ this.singleResourceComponentRef = null;
815
+ this.singleResourceCacheIdentity = null;
816
+ // Clear the host container's DOM so the user isn't briefly looking at the
817
+ // destroyed component's wrapper between CloseTab and the default-tab load.
818
+ const directContainer = this.directContentContainer?.nativeElement;
819
+ if (directContainer) {
820
+ while (directContainer.firstChild)
821
+ directContainer.removeChild(directContainer.firstChild);
822
+ }
823
+ }
824
+ else {
825
+ // Multi-tab mode: cache entry is keyed by attachedToTabId; the convenience
826
+ // method handles the lookup.
827
+ this.cacheManager.destroyComponentByTabId(tabId);
828
+ this.componentRefs.delete(tabId);
829
+ }
830
+ this.workspaceManager.CloseTab(tabId);
831
+ if (isLastTab && tab) {
832
+ // CloseTab kept the tab around but unpinned it. Replace it with the app's
833
+ // default tab so the user lands on a meaningful surface (typically the
834
+ // home dashboard) instead of staying on the discarded form.
835
+ const app = this.appManager.GetAppById(tab.applicationId);
836
+ if (app) {
837
+ const defaultTabRequest = await app.CreateDefaultTab();
838
+ if (defaultTabRequest) {
839
+ this.workspaceManager.OpenTab(defaultTabRequest, app.GetColor());
840
+ }
841
+ }
842
+ }
843
+ }
674
844
  /**
675
845
  * Generate a signature for tab content to detect when content changes
676
846
  * This is needed because in single-resource mode, the same tab ID can have different content
@@ -753,13 +923,16 @@ export class TabContainerComponent extends BaseAngularComponent {
753
923
  glContainer.element.innerHTML = '';
754
924
  // Get driver class for cache lookup (resolves to actual component class name)
755
925
  const driverClass = resourceData.Configuration?.resourceTypeDriverClass || resourceData.ResourceType;
926
+ // Discriminate distinct "new record" tabs of different entities (all have empty
927
+ // ResourceRecordID otherwise — would silently collide in the cache).
928
+ const cacheDiscriminator = resourceData.Configuration?.Entity;
756
929
  // Check if we have a cached component for this resource
757
- const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', tab.applicationId);
930
+ const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', tab.applicationId, cacheDiscriminator);
758
931
  if (cached) {
759
932
  // Reattach the cached wrapper element
760
933
  glContainer.element.appendChild(cached.wrapperElement);
761
934
  // Mark as attached to this tab
762
- this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', tab.applicationId, tabId);
935
+ this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', tab.applicationId, tabId, cacheDiscriminator);
763
936
  // Keep legacy componentRefs map updated
764
937
  this.componentRefs.set(tabId, cached.componentRef);
765
938
  // If resource is already loaded, update tab title immediately and signal
@@ -802,10 +975,11 @@ export class TabContainerComponent extends BaseAngularComponent {
802
975
  this.emitFirstLoadCompleteOnce();
803
976
  };
804
977
  instance.ResourceRecordSavedEvent = (entity) => {
805
- // Update tab title if needed
806
- if (entity && entity.Get && entity.Get('Name')) {
807
- // TODO: Implement UpdateTabTitle in WorkspaceStateManager
808
- }
978
+ this.handleResourceRecordSaved(driverClass, tab.applicationId, tabId, instance, entity);
979
+ };
980
+ // Resource asked to be closed (e.g., user discarded a brand-new record).
981
+ instance.ResourceCloseRequestedEvent = () => {
982
+ this.handleResourceCloseRequested(tabId, instance);
809
983
  };
810
984
  // Wire up display name change notifications
811
985
  instance.DisplayNameChangedEvent = (newName) => {