@memberjunction/ng-explorer-core 5.22.0 → 5.24.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 (91) hide show
  1. package/dist/app-routing.module.d.ts.map +1 -1
  2. package/dist/app-routing.module.js +1 -3
  3. package/dist/app-routing.module.js.map +1 -1
  4. package/dist/generated/lazy-feature-config.d.ts +1 -1
  5. package/dist/generated/lazy-feature-config.d.ts.map +1 -1
  6. package/dist/generated/lazy-feature-config.js +5 -2
  7. package/dist/generated/lazy-feature-config.js.map +1 -1
  8. package/dist/lib/dashboard-preferences-dialog/dashboard-preferences-dialog.component.js +49 -49
  9. package/dist/lib/dashboard-preferences-dialog/dashboard-preferences-dialog.component.js.map +1 -1
  10. package/dist/lib/oauth/oauth-callback.component.js +6 -6
  11. package/dist/lib/oauth/oauth-callback.component.js.map +1 -1
  12. package/dist/lib/oauth/oauth.module.d.ts +2 -3
  13. package/dist/lib/oauth/oauth.module.d.ts.map +1 -1
  14. package/dist/lib/oauth/oauth.module.js +0 -4
  15. package/dist/lib/oauth/oauth.module.js.map +1 -1
  16. package/dist/lib/resource-wrappers/chat-collections-resource.component.d.ts +6 -25
  17. package/dist/lib/resource-wrappers/chat-collections-resource.component.d.ts.map +1 -1
  18. package/dist/lib/resource-wrappers/chat-collections-resource.component.js +30 -124
  19. package/dist/lib/resource-wrappers/chat-collections-resource.component.js.map +1 -1
  20. package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts +7 -23
  21. package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts.map +1 -1
  22. package/dist/lib/resource-wrappers/chat-conversations-resource.component.js +50 -142
  23. package/dist/lib/resource-wrappers/chat-conversations-resource.component.js.map +1 -1
  24. package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts +3 -19
  25. package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts.map +1 -1
  26. package/dist/lib/resource-wrappers/chat-tasks-resource.component.js +16 -98
  27. package/dist/lib/resource-wrappers/chat-tasks-resource.component.js.map +1 -1
  28. package/dist/lib/resource-wrappers/list-detail-resource.component.d.ts +3 -1
  29. package/dist/lib/resource-wrappers/list-detail-resource.component.d.ts.map +1 -1
  30. package/dist/lib/resource-wrappers/list-detail-resource.component.js +3 -0
  31. package/dist/lib/resource-wrappers/list-detail-resource.component.js.map +1 -1
  32. package/dist/lib/resource-wrappers/notifications-resource.component.js +1 -0
  33. package/dist/lib/resource-wrappers/notifications-resource.component.js.map +1 -1
  34. package/dist/lib/resource-wrappers/view-resource.component.d.ts +13 -11
  35. package/dist/lib/resource-wrappers/view-resource.component.d.ts.map +1 -1
  36. package/dist/lib/resource-wrappers/view-resource.component.js +80 -89
  37. package/dist/lib/resource-wrappers/view-resource.component.js.map +1 -1
  38. package/dist/lib/services/startup-validation.service.d.ts.map +1 -1
  39. package/dist/lib/services/startup-validation.service.js +0 -3
  40. package/dist/lib/services/startup-validation.service.js.map +1 -1
  41. package/dist/lib/shell/components/header/app-nav.component.d.ts.map +1 -1
  42. package/dist/lib/shell/components/header/app-nav.component.js +18 -3
  43. package/dist/lib/shell/components/header/app-nav.component.js.map +1 -1
  44. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +45 -16
  45. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
  46. package/dist/lib/shell/components/tabs/component-cache-manager.js +57 -35
  47. package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
  48. package/dist/lib/shell/components/tabs/tab-container.component.d.ts +33 -0
  49. package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
  50. package/dist/lib/shell/components/tabs/tab-container.component.js +99 -17
  51. package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
  52. package/dist/lib/shell/services/settings-dialog.service.d.ts +8 -8
  53. package/dist/lib/shell/services/settings-dialog.service.d.ts.map +1 -1
  54. package/dist/lib/shell/services/settings-dialog.service.js +20 -26
  55. package/dist/lib/shell/services/settings-dialog.service.js.map +1 -1
  56. package/dist/lib/shell/shell.component.d.ts.map +1 -1
  57. package/dist/lib/shell/shell.component.js +17 -17
  58. package/dist/lib/shell/shell.component.js.map +1 -1
  59. package/dist/lib/shell/shell.module.d.ts +4 -5
  60. package/dist/lib/shell/shell.module.d.ts.map +1 -1
  61. package/dist/lib/shell/shell.module.js +4 -8
  62. package/dist/lib/shell/shell.module.js.map +1 -1
  63. package/dist/lib/single-dashboard/Components/add-item/add-item.component.js +72 -71
  64. package/dist/lib/single-dashboard/Components/add-item/add-item.component.js.map +1 -1
  65. package/dist/lib/single-dashboard/Components/delete-item/delete-item.component.js +11 -11
  66. package/dist/lib/single-dashboard/Components/delete-item/delete-item.component.js.map +1 -1
  67. package/dist/lib/single-dashboard/Components/edit-dashboard/edit-dashboard.component.d.ts +36 -12
  68. package/dist/lib/single-dashboard/Components/edit-dashboard/edit-dashboard.component.d.ts.map +1 -1
  69. package/dist/lib/single-dashboard/Components/edit-dashboard/edit-dashboard.component.js +78 -50
  70. package/dist/lib/single-dashboard/Components/edit-dashboard/edit-dashboard.component.js.map +1 -1
  71. package/dist/lib/single-dashboard/single-dashboard.component.d.ts +12 -5
  72. package/dist/lib/single-dashboard/single-dashboard.component.d.ts.map +1 -1
  73. package/dist/lib/single-dashboard/single-dashboard.component.js +44 -55
  74. package/dist/lib/single-dashboard/single-dashboard.component.js.map +1 -1
  75. package/dist/lib/single-list-detail/single-list-detail.component.d.ts +10 -2
  76. package/dist/lib/single-list-detail/single-list-detail.component.d.ts.map +1 -1
  77. package/dist/lib/single-list-detail/single-list-detail.component.js +313 -243
  78. package/dist/lib/single-list-detail/single-list-detail.component.js.map +1 -1
  79. package/dist/module.d.ts +51 -63
  80. package/dist/module.d.ts.map +1 -1
  81. package/dist/module.js +35 -80
  82. package/dist/module.js.map +1 -1
  83. package/dist/public-api.d.ts +0 -1
  84. package/dist/public-api.d.ts.map +1 -1
  85. package/dist/public-api.js +0 -1
  86. package/dist/public-api.js.map +1 -1
  87. package/package.json +38 -47
  88. package/dist/lib/generic/form-toolbar.d.ts +0 -8
  89. package/dist/lib/generic/form-toolbar.d.ts.map +0 -1
  90. package/dist/lib/generic/form-toolbar.js +0 -114
  91. package/dist/lib/generic/form-toolbar.js.map +0 -1
@@ -35,10 +35,8 @@ export class StartupValidationService {
35
35
  */
36
36
  validateUserRoles() {
37
37
  try {
38
- console.log('StartupValidationService: Validating user roles');
39
38
  const md = new Metadata();
40
39
  const currentUser = md.CurrentUser;
41
- console.log('StartupValidationService: Current user:', currentUser);
42
40
  if (!currentUser) {
43
41
  console.log('StartupValidationService: No current user found');
44
42
  this.validationService.addIssue({
@@ -50,7 +48,6 @@ export class StartupValidationService {
50
48
  });
51
49
  return;
52
50
  }
53
- console.log('StartupValidationService: User roles:', currentUser.UserRoles);
54
51
  if (!currentUser.UserRoles || currentUser.UserRoles.length === 0) {
55
52
  console.log('StartupValidationService: No user roles found, adding validation issue');
56
53
  this.validationService.addIssue({
@@ -1 +1 @@
1
- {"version":3,"file":"startup-validation.service.js","sourceRoot":"","sources":["../../../src/lib/services/startup-validation.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAY,MAAM,sBAAsB,CAAC;;;AAG1D;;GAEG;AAIH,MAAM,OAAO,wBAAwB;IACf;IAApB,YAAoB,iBAA0C;QAA1C,sBAAiB,GAAjB,iBAAiB,CAAyB;IAAI,CAAC;IAEnE;;OAEG;IACI,mBAAmB;QACxB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACzB,4CAA4C;IAC9C,CAAC;IAED;;;OAGG;IACI,yBAAyB;QAC9B,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC;YAC9B,EAAE,EAAE,eAAe;YACnB,OAAO,EAAE,gDAAgD;YACzD,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,kKAAkK;YAC3K,IAAI,EAAE,mIAAmI;SAC1I,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,iBAAiB;QACvB,IAAI,CAAC;YACH,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAC;YAC/D,MAAM,EAAE,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC1B,MAAM,WAAW,GAAG,EAAE,CAAC,WAAW,CAAC;YAEnC,OAAO,CAAC,GAAG,CAAC,yCAAyC,EAAE,WAAW,CAAC,CAAC;YAEpE,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAC;gBAC/D,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC;oBAC9B,EAAE,EAAE,gBAAgB;oBACpB,OAAO,EAAE,wBAAwB;oBACjC,QAAQ,EAAE,OAAO;oBACjB,OAAO,EAAE,qDAAqD;oBAC9D,IAAI,EAAE,8EAA8E;iBACrF,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YAED,OAAO,CAAC,GAAG,CAAC,uCAAuC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;YAE5E,IAAI,CAAC,WAAW,CAAC,SAAS,IAAI,WAAW,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACjE,OAAO,CAAC,GAAG,CAAC,wEAAwE,CAAC,CAAC;gBACtF,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC;oBAC9B,EAAE,EAAE,eAAe;oBACnB,OAAO,EAAE,mCAAmC;oBAC5C,QAAQ,EAAE,OAAO;oBACjB,OAAO,EAAE,8GAA8G;oBACvH,IAAI,EAAE,uFAAuF;iBAC9F,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,6DAA6D,EAAE,GAAG,CAAC,CAAC;YAClF,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC;gBAC9B,EAAE,EAAE,yBAAyB;gBAC7B,OAAO,EAAE,4BAA4B;gBACrC,QAAQ,EAAE,OAAO;gBACjB,OAAO,EAAE,mDAAmD;gBAC5D,IAAI,EAAE,sFAAsF;aAC7F,CAAC,CAAC;QACL,CAAC;IACH,CAAC;kHAtEU,wBAAwB;gEAAxB,wBAAwB,WAAxB,wBAAwB,mBAFvB,MAAM;;iFAEP,wBAAwB;cAHpC,UAAU;eAAC;gBACV,UAAU,EAAE,MAAM;aACnB","sourcesContent":["import { Injectable } from '@angular/core';\nimport { Metadata, UserInfo } from '@memberjunction/core';\nimport { SystemValidationService } from './system-validation.service';\n\n/**\n * Service that performs validation checks during application startup\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class StartupValidationService {\n constructor(private validationService: SystemValidationService) { }\n \n /**\n * Runs all validation checks\n */\n public validateSystemSetup(): void {\n this.validateUserRoles();\n // Add more validation checks here as needed\n }\n \n /**\n * Directly adds a validation issue for no user roles\n * This is used when we detect the missing roles error during GraphQL setup\n */\n public addNoRolesValidationIssue(): void {\n this.validationService.addIssue({\n id: 'no-user-roles',\n message: 'Missing User Roles - Cannot Access Application',\n severity: 'error',\n details: 'Your account does not have any roles assigned, which is required to use the application. This is preventing the system from loading core resources and metadata.',\n help: 'Please contact your system administrator to have appropriate roles assigned to your account. At minimum, a \"UI\" role is required.'\n });\n }\n\n /**\n * Validates that the current user has at least one role assigned\n */\n private validateUserRoles(): void {\n try {\n console.log('StartupValidationService: Validating user roles');\n const md = new Metadata();\n const currentUser = md.CurrentUser;\n \n console.log('StartupValidationService: Current user:', currentUser);\n \n if (!currentUser) {\n console.log('StartupValidationService: No current user found');\n this.validationService.addIssue({\n id: 'user-not-found',\n message: 'User account not found',\n severity: 'error',\n details: 'Your user account could not be found in the system.',\n help: 'Contact your system administrator to ensure your account is properly set up.'\n });\n return;\n }\n \n console.log('StartupValidationService: User roles:', currentUser.UserRoles);\n \n if (!currentUser.UserRoles || currentUser.UserRoles.length === 0) {\n console.log('StartupValidationService: No user roles found, adding validation issue');\n this.validationService.addIssue({\n id: 'no-user-roles',\n message: 'No roles assigned to your account',\n severity: 'error',\n details: 'Your user account does not have any roles assigned. This will prevent you from accessing most functionality.',\n help: 'Contact your system administrator to have appropriate roles assigned to your account.'\n });\n }\n } catch (err) {\n console.error('StartupValidationService: Error during user role validation', err);\n this.validationService.addIssue({\n id: 'user-roles-check-failed',\n message: 'Failed to check user roles',\n severity: 'error',\n details: 'An error occurred while checking your user roles.',\n help: 'Try refreshing the page. If the problem persists, contact your system administrator.'\n });\n }\n }\n}"]}
1
+ {"version":3,"file":"startup-validation.service.js","sourceRoot":"","sources":["../../../src/lib/services/startup-validation.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAY,MAAM,sBAAsB,CAAC;;;AAG1D;;GAEG;AAIH,MAAM,OAAO,wBAAwB;IACf;IAApB,YAAoB,iBAA0C;QAA1C,sBAAiB,GAAjB,iBAAiB,CAAyB;IAAI,CAAC;IAEnE;;OAEG;IACI,mBAAmB;QACxB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACzB,4CAA4C;IAC9C,CAAC;IAED;;;OAGG;IACI,yBAAyB;QAC9B,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC;YAC9B,EAAE,EAAE,eAAe;YACnB,OAAO,EAAE,gDAAgD;YACzD,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,kKAAkK;YAC3K,IAAI,EAAE,mIAAmI;SAC1I,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,iBAAiB;QACvB,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC1B,MAAM,WAAW,GAAG,EAAE,CAAC,WAAW,CAAC;YAEnC,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAC;gBAC/D,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC;oBAC9B,EAAE,EAAE,gBAAgB;oBACpB,OAAO,EAAE,wBAAwB;oBACjC,QAAQ,EAAE,OAAO;oBACjB,OAAO,EAAE,qDAAqD;oBAC9D,IAAI,EAAE,8EAA8E;iBACrF,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YAED,IAAI,CAAC,WAAW,CAAC,SAAS,IAAI,WAAW,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACjE,OAAO,CAAC,GAAG,CAAC,wEAAwE,CAAC,CAAC;gBACtF,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC;oBAC9B,EAAE,EAAE,eAAe;oBACnB,OAAO,EAAE,mCAAmC;oBAC5C,QAAQ,EAAE,OAAO;oBACjB,OAAO,EAAE,8GAA8G;oBACvH,IAAI,EAAE,uFAAuF;iBAC9F,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,6DAA6D,EAAE,GAAG,CAAC,CAAC;YAClF,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC;gBAC9B,EAAE,EAAE,yBAAyB;gBAC7B,OAAO,EAAE,4BAA4B;gBACrC,QAAQ,EAAE,OAAO;gBACjB,OAAO,EAAE,mDAAmD;gBAC5D,IAAI,EAAE,sFAAsF;aAC7F,CAAC,CAAC;QACL,CAAC;IACH,CAAC;kHAjEU,wBAAwB;gEAAxB,wBAAwB,WAAxB,wBAAwB,mBAFvB,MAAM;;iFAEP,wBAAwB;cAHpC,UAAU;eAAC;gBACV,UAAU,EAAE,MAAM;aACnB","sourcesContent":["import { Injectable } from '@angular/core';\nimport { Metadata, UserInfo } from '@memberjunction/core';\nimport { SystemValidationService } from './system-validation.service';\n\n/**\n * Service that performs validation checks during application startup\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class StartupValidationService {\n constructor(private validationService: SystemValidationService) { }\n \n /**\n * Runs all validation checks\n */\n public validateSystemSetup(): void {\n this.validateUserRoles();\n // Add more validation checks here as needed\n }\n \n /**\n * Directly adds a validation issue for no user roles\n * This is used when we detect the missing roles error during GraphQL setup\n */\n public addNoRolesValidationIssue(): void {\n this.validationService.addIssue({\n id: 'no-user-roles',\n message: 'Missing User Roles - Cannot Access Application',\n severity: 'error',\n details: 'Your account does not have any roles assigned, which is required to use the application. This is preventing the system from loading core resources and metadata.',\n help: 'Please contact your system administrator to have appropriate roles assigned to your account. At minimum, a \"UI\" role is required.'\n });\n }\n\n /**\n * Validates that the current user has at least one role assigned\n */\n private validateUserRoles(): void {\n try {\n const md = new Metadata();\n const currentUser = md.CurrentUser;\n \n if (!currentUser) {\n console.log('StartupValidationService: No current user found');\n this.validationService.addIssue({\n id: 'user-not-found',\n message: 'User account not found',\n severity: 'error',\n details: 'Your user account could not be found in the system.',\n help: 'Contact your system administrator to ensure your account is properly set up.'\n });\n return;\n }\n \n if (!currentUser.UserRoles || currentUser.UserRoles.length === 0) {\n console.log('StartupValidationService: No user roles found, adding validation issue');\n this.validationService.addIssue({\n id: 'no-user-roles',\n message: 'No roles assigned to your account',\n severity: 'error',\n details: 'Your user account does not have any roles assigned. This will prevent you from accessing most functionality.',\n help: 'Contact your system administrator to have appropriate roles assigned to your account.'\n });\n }\n } catch (err) {\n console.error('StartupValidationService: Error during user role validation', err);\n this.validationService.addIssue({\n id: 'user-roles-check-failed',\n message: 'Failed to check user roles',\n severity: 'error',\n details: 'An error occurred while checking your user roles.',\n help: 'Try refreshing the page. If the problem persists, contact your system administrator.'\n });\n }\n }\n}"]}
@@ -1 +1 @@
1
- {"version":3,"file":"app-nav.component.d.ts","sourceRoot":"","sources":["../../../../../src/lib/shell/components/header/app-nav.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,iBAAiB,EAA2B,MAAM,eAAe,CAAC;AACtI,OAAO,EAAE,eAAe,EAAkB,OAAO,EAAE,qBAAqB,EAA0B,MAAM,qCAAqC,CAAC;AAC9I,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;;AAG1D;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,qBAOa,eAAgB,YAAW,MAAM,EAAE,SAAS;IA8BrD,OAAO,CAAC,gBAAgB;IACxB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,GAAG;IA/Bb,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,IAAI,CAAgC;IAC5C,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,iBAAiB,CAAS;IAElC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,iBAAiB,CAAK;IAG9B,OAAO,CAAC,cAAc,CAA8B;IAE1C,YAAY,kCAAyC;IACrD,cAAc,wBAA+B;gBAG7C,gBAAgB,EAAE,qBAAqB,EACvC,aAAa,EAAE,aAAa,EAC5B,GAAG,EAAE,iBAAiB;IAGhC;;OAEG;IACH,IACI,GAAG,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,EASpC;IAED,IAAI,GAAG,IAAI,eAAe,GAAG,IAAI,CAEhC;IAED,QAAQ,IAAI,IAAI;IAahB,WAAW,IAAI,IAAI;IAKnB;;OAEG;YACW,gBAAgB;IA6C9B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAoB1B;;;OAGG;IACH,OAAO,CAAC,UAAU;IAIlB;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO;IAIjC;;OAEG;IACH,OAAO,CAAC,eAAe;IAYvB;;OAEG;IACH,IAAI,QAAQ,IAAI,OAAO,EAAE,CAExB;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO;IAKhC;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM;IAIrD;;OAEG;IACH,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,IAAI;IAOnD;;;;OAIG;IACH,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI;yCAzNtC,eAAe;2CAAf,eAAe;CA0O3B"}
1
+ {"version":3,"file":"app-nav.component.d.ts","sourceRoot":"","sources":["../../../../../src/lib/shell/components/header/app-nav.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,iBAAiB,EAA2B,MAAM,eAAe,CAAC;AACtI,OAAO,EAAE,eAAe,EAAkB,OAAO,EAAE,qBAAqB,EAA0B,MAAM,qCAAqC,CAAC;AAC9I,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;;AAG1D;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,qBAOa,eAAgB,YAAW,MAAM,EAAE,SAAS;IA8BrD,OAAO,CAAC,gBAAgB;IACxB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,GAAG;IA/Bb,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,IAAI,CAAgC;IAC5C,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,eAAe,CAAqC;IAC5D,OAAO,CAAC,iBAAiB,CAAS;IAElC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,iBAAiB,CAAK;IAG9B,OAAO,CAAC,cAAc,CAA8B;IAE1C,YAAY,kCAAyC;IACrD,cAAc,wBAA+B;gBAG7C,gBAAgB,EAAE,qBAAqB,EACvC,aAAa,EAAE,aAAa,EAC5B,GAAG,EAAE,iBAAiB;IAGhC;;OAEG;IACH,IACI,GAAG,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,EASpC;IAED,IAAI,GAAG,IAAI,eAAe,GAAG,IAAI,CAEhC;IAED,QAAQ,IAAI,IAAI;IAahB,WAAW,IAAI,IAAI;IAKnB;;OAEG;YACW,gBAAgB;IA6C9B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAoB1B;;;OAGG;IACH,OAAO,CAAC,UAAU;IAIlB;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO;IAIjC;;OAEG;IACH,OAAO,CAAC,eAAe;IA+BvB;;OAEG;IACH,IAAI,QAAQ,IAAI,OAAO,EAAE,CAExB;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO;IAKhC;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM;IAIrD;;OAEG;IACH,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,IAAI;IAOnD;;;;OAIG;IACH,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI;yCA5OtC,eAAe;2CAAf,eAAe;CA6P3B"}
@@ -197,9 +197,24 @@ export class AppNavComponent {
197
197
  if (dynamicItem.isActiveMatch && typeof dynamicItem.isActiveMatch === 'function') {
198
198
  return dynamicItem.isActiveMatch(activeTab);
199
199
  }
200
- // Standard matching: route or label
201
- return (item.Route && activeTab.configuration['route'] === item.Route) ||
202
- activeTab.title === item.Label;
200
+ const config = activeTab.configuration || {};
201
+ // Match by DriverClass (most reliable for Custom resource types — always set correctly)
202
+ if (item.DriverClass && (config['driverClass'] === item.DriverClass || config['resourceTypeDriverClass'] === item.DriverClass)) {
203
+ return true;
204
+ }
205
+ // Match by navItemName from config (reliable — set when nav item opens)
206
+ if (config['navItemName'] && config['navItemName'] === item.Label) {
207
+ return true;
208
+ }
209
+ // Match by route (for route-based nav items)
210
+ if (item.Route && config['route'] === item.Route) {
211
+ return true;
212
+ }
213
+ // NOTE: We intentionally do NOT match by activeTab.title here.
214
+ // Tab titles can be stale (updated asynchronously by DisplayNameChangedEvent
215
+ // from cached components) and cause double-matches where two nav items
216
+ // both appear active. DriverClass and navItemName are sufficient.
217
+ return false;
203
218
  }
204
219
  /**
205
220
  * Get cached navigation items (no computation in getter)
@@ -1 +1 @@
1
- {"version":3,"file":"app-nav.component.js","sourceRoot":"","sources":["../../../../../src/lib/shell/components/header/app-nav.component.ts","../../../../../src/lib/shell/components/header/app-nav.component.html"],"names":[],"mappings":"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","sourcesContent":["import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';\nimport { BaseApplication, DynamicNavItem, NavItem, WorkspaceStateManager, WorkspaceConfiguration } from '@memberjunction/ng-base-application';\nimport { SharedService } from '@memberjunction/ng-shared';\nimport { Subject, takeUntil } from 'rxjs';\n\n/**\n * Event emitted when a nav item is clicked\n */\nexport interface NavItemClickEvent {\n item: NavItem;\n shiftKey: boolean;\n}\n\n/**\n * Horizontal navigation items for the current app.\n * Uses OnPush change detection and reactive state management for optimal performance.\n */\n@Component({\n standalone: false,\n selector: 'mj-app-nav',\n templateUrl: './app-nav.component.html',\n styleUrls: ['./app-nav.component.css'],\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class AppNavComponent implements OnInit, OnDestroy {\n private destroy$ = new Subject<void>();\n private _app: BaseApplication | null = null;\n private _cachedNavItems: NavItem[] = [];\n private _cachedAppColor: string = 'var(--mj-brand-primary)';\n private _servicesInjected = false;\n\n /**\n * Monotonically increasing counter used to detect and discard stale async results.\n *\n * Because GetNavItems() is async (HomeApplication does a DB lookup for record names),\n * and RxJS subscribe() does NOT serialize async callbacks, multiple calls to\n * updateCachedData() can overlap. Without this guard, a slow call (e.g., Home app\n * doing a DB lookup) that started BEFORE a fast call (e.g., switching to App B)\n * could resolve AFTER the fast call and overwrite the correct nav items with stale ones.\n *\n * How it works:\n * 1. Each updateCachedData() call increments this counter and captures it as `gen`\n * 2. After the await, it checks: does `gen` still match `_updateGeneration`?\n * 3. If not, a newer call started while we were waiting — discard our stale results\n */\n private _updateGeneration = 0;\n\n // Map of nav item key (Route or Label) to active state\n private activeStateMap = new Map<string, boolean>();\n\n @Output() navItemClick = new EventEmitter<NavItemClickEvent>();\n @Output() navItemDismiss = new EventEmitter<NavItem>();\n\n constructor(\n private workspaceManager: WorkspaceStateManager,\n private sharedService: SharedService,\n private cdr: ChangeDetectorRef\n ) {}\n\n /**\n * Input setter for app - triggers cache update when app changes\n */\n @Input()\n set app(value: BaseApplication | null) {\n if (this._app !== value) {\n this._app = value;\n this._cachedNavItems = []; // Clear stale items immediately so previous app's items don't flash\n this.activeStateMap.clear();\n this._servicesInjected = false; // Reset injection flag\n this.updateCachedData();\n this.cdr.markForCheck();\n }\n }\n\n get app(): BaseApplication | null {\n return this._app;\n }\n\n ngOnInit(): void {\n // Subscribe to workspace configuration changes.\n // Must rebuild nav items (not just active states) because dynamic nav items\n // are generated based on the currently active tab - when a user navigates\n // from one record to another (e.g., via OpenEntityRecord), the active tab\n // changes and the dynamic nav item needs to reflect the new record.\n this.workspaceManager.Configuration\n .pipe(takeUntil(this.destroy$))\n .subscribe(async () => {\n await this.updateCachedData();\n });\n }\n\n ngOnDestroy(): void {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n /**\n * Update cached nav items and app color when app changes\n */\n private async updateCachedData(): Promise<void> {\n // Capture the current generation before any async work.\n // See _updateGeneration JSDoc for full explanation of the race condition this prevents.\n const gen = ++this._updateGeneration;\n\n if (this._app) {\n // Inject services once for apps that need them (e.g., HomeApplication for dynamic nav items)\n if (!this._servicesInjected) {\n const appWithServices = this._app as BaseApplication & {\n SetWorkspaceManager?: (manager: WorkspaceStateManager) => void;\n SetSharedService?: (service: SharedService) => void;\n };\n\n if (typeof appWithServices.SetWorkspaceManager === 'function') {\n appWithServices.SetWorkspaceManager(this.workspaceManager);\n }\n if (typeof appWithServices.SetSharedService === 'function') {\n appWithServices.SetSharedService(this.sharedService);\n }\n this._servicesInjected = true;\n }\n\n const items = await this._app.GetNavItems() || [];\n\n // If a newer call started while we were awaiting, our results are stale — bail out\n // so we don't overwrite the newer call's (correct) results.\n if (gen !== this._updateGeneration) {\n return;\n }\n\n // Only show items with Status 'Active' or undefined (default to Active)\n this._cachedNavItems = items.filter(item => !item.Status || item.Status === 'Active');\n\n this._cachedAppColor = this._app.GetColor() || 'var(--mj-brand-primary)';\n } else {\n this._cachedNavItems = [];\n this._cachedAppColor = 'var(--mj-brand-primary)';\n }\n\n // Update active states after nav items change\n const config = this.workspaceManager.GetConfiguration();\n this.updateActiveStates(config);\n this.cdr.markForCheck();\n }\n\n /**\n * Update active state map based on current workspace configuration\n */\n private updateActiveStates(config: WorkspaceConfiguration | null): void {\n this.activeStateMap.clear();\n\n if (!config || !this._app) {\n return;\n }\n\n const activeTab = config.tabs.find(t => t.id === config.activeTabId);\n if (!activeTab || activeTab.applicationId !== this._app.ID) {\n return;\n }\n\n // Compute active state for each nav item once\n for (const item of this._cachedNavItems) {\n const key = this.getItemKey(item);\n const isActive = this.computeIsActive(item, activeTab);\n this.activeStateMap.set(key, isActive);\n }\n }\n\n /**\n * Get unique key for nav item (used for tracking and active state).\n * Prefers RecordID for dynamic items to avoid label collisions.\n */\n private getItemKey(item: NavItem): string {\n return item.RecordID || item.Route || item.Label || '';\n }\n\n /**\n * Check if a nav item is dynamic (generated from recent orphan resources)\n */\n isDynamic(item: NavItem): boolean {\n return (item as DynamicNavItem).isDynamic === true;\n }\n\n /**\n * Compute if nav item is active based on active tab\n */\n private computeIsActive(item: NavItem, activeTab: any): boolean {\n // Check if nav item has a custom matching function (for dynamic items)\n const dynamicItem = item as NavItem & { isActiveMatch?: (tab: unknown) => boolean };\n if (dynamicItem.isActiveMatch && typeof dynamicItem.isActiveMatch === 'function') {\n return dynamicItem.isActiveMatch(activeTab);\n }\n\n // Standard matching: route or label\n return (item.Route && activeTab.configuration['route'] === item.Route) ||\n activeTab.title === item.Label;\n }\n\n /**\n * Get cached navigation items (no computation in getter)\n */\n get navItems(): NavItem[] {\n return this._cachedNavItems;\n }\n\n /**\n * Get cached app color (no computation in getter)\n */\n get appColor(): string {\n return this._cachedAppColor;\n }\n\n /**\n * Check if nav item is active (uses cached state from Map)\n */\n isActive(item: NavItem): boolean {\n const key = this.getItemKey(item);\n return this.activeStateMap.get(key) || false;\n }\n\n /**\n * Track function for @for to optimize rendering\n */\n trackByNavItem(_index: number, item: NavItem): string {\n return this.getItemKey(item);\n }\n\n /**\n * Handle nav item click\n */\n onNavClick(item: NavItem, event?: MouseEvent): void {\n this.navItemClick.emit({\n item,\n shiftKey: event?.shiftKey || false\n });\n }\n\n /**\n * Handle dismiss click on a dynamic nav item.\n * Removes from the app's recent stack and refreshes nav items immediately.\n * Stops propagation so the nav click handler doesn't fire.\n */\n onDismiss(item: NavItem, event: MouseEvent): void {\n event.stopPropagation();\n\n // Remove from the app's recent stack directly so we can refresh immediately\n if (this._app) {\n const appWithRemove = this._app as BaseApplication & {\n RemoveDynamicNavItem?: (navItem: NavItem) => void;\n };\n if (typeof appWithRemove.RemoveDynamicNavItem === 'function') {\n appWithRemove.RemoveDynamicNavItem(item);\n this.updateCachedData();\n }\n }\n\n this.navItemDismiss.emit(item);\n }\n\n}\n","<nav class=\"nav-list\" [style.--app-color]=\"appColor\">\n @for (item of navItems; track trackByNavItem($index, item)) {\n <div\n class=\"nav-item\"\n [class.active]=\"isActive(item)\"\n [class.dynamic]=\"isDynamic(item)\"\n [class.no-icon]=\"!item.Icon\"\n (click)=\"onNavClick(item, $event)\">\n @if (item.Icon) {\n <i [class]=\"item.Icon\"></i>\n }\n <span>{{ item.Label }}</span>\n @if (item.Badge) {\n <span class=\"badge\">{{ item.Badge }}</span>\n }\n @if (isDynamic(item)) {\n <button class=\"dismiss-btn\" title=\"Remove\" (click)=\"onDismiss(item, $event)\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n }\n </div>\n }\n</nav>\n"]}
1
+ {"version":3,"file":"app-nav.component.js","sourceRoot":"","sources":["../../../../../src/lib/shell/components/header/app-nav.component.ts","../../../../../src/lib/shell/components/header/app-nav.component.html"],"names":[],"mappings":"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","sourcesContent":["import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';\nimport { BaseApplication, DynamicNavItem, NavItem, WorkspaceStateManager, WorkspaceConfiguration } from '@memberjunction/ng-base-application';\nimport { SharedService } from '@memberjunction/ng-shared';\nimport { Subject, takeUntil } from 'rxjs';\n\n/**\n * Event emitted when a nav item is clicked\n */\nexport interface NavItemClickEvent {\n item: NavItem;\n shiftKey: boolean;\n}\n\n/**\n * Horizontal navigation items for the current app.\n * Uses OnPush change detection and reactive state management for optimal performance.\n */\n@Component({\n standalone: false,\n selector: 'mj-app-nav',\n templateUrl: './app-nav.component.html',\n styleUrls: ['./app-nav.component.css'],\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class AppNavComponent implements OnInit, OnDestroy {\n private destroy$ = new Subject<void>();\n private _app: BaseApplication | null = null;\n private _cachedNavItems: NavItem[] = [];\n private _cachedAppColor: string = 'var(--mj-brand-primary)';\n private _servicesInjected = false;\n\n /**\n * Monotonically increasing counter used to detect and discard stale async results.\n *\n * Because GetNavItems() is async (HomeApplication does a DB lookup for record names),\n * and RxJS subscribe() does NOT serialize async callbacks, multiple calls to\n * updateCachedData() can overlap. Without this guard, a slow call (e.g., Home app\n * doing a DB lookup) that started BEFORE a fast call (e.g., switching to App B)\n * could resolve AFTER the fast call and overwrite the correct nav items with stale ones.\n *\n * How it works:\n * 1. Each updateCachedData() call increments this counter and captures it as `gen`\n * 2. After the await, it checks: does `gen` still match `_updateGeneration`?\n * 3. If not, a newer call started while we were waiting — discard our stale results\n */\n private _updateGeneration = 0;\n\n // Map of nav item key (Route or Label) to active state\n private activeStateMap = new Map<string, boolean>();\n\n @Output() navItemClick = new EventEmitter<NavItemClickEvent>();\n @Output() navItemDismiss = new EventEmitter<NavItem>();\n\n constructor(\n private workspaceManager: WorkspaceStateManager,\n private sharedService: SharedService,\n private cdr: ChangeDetectorRef\n ) {}\n\n /**\n * Input setter for app - triggers cache update when app changes\n */\n @Input()\n set app(value: BaseApplication | null) {\n if (this._app !== value) {\n this._app = value;\n this._cachedNavItems = []; // Clear stale items immediately so previous app's items don't flash\n this.activeStateMap.clear();\n this._servicesInjected = false; // Reset injection flag\n this.updateCachedData();\n this.cdr.markForCheck();\n }\n }\n\n get app(): BaseApplication | null {\n return this._app;\n }\n\n ngOnInit(): void {\n // Subscribe to workspace configuration changes.\n // Must rebuild nav items (not just active states) because dynamic nav items\n // are generated based on the currently active tab - when a user navigates\n // from one record to another (e.g., via OpenEntityRecord), the active tab\n // changes and the dynamic nav item needs to reflect the new record.\n this.workspaceManager.Configuration\n .pipe(takeUntil(this.destroy$))\n .subscribe(async () => {\n await this.updateCachedData();\n });\n }\n\n ngOnDestroy(): void {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n /**\n * Update cached nav items and app color when app changes\n */\n private async updateCachedData(): Promise<void> {\n // Capture the current generation before any async work.\n // See _updateGeneration JSDoc for full explanation of the race condition this prevents.\n const gen = ++this._updateGeneration;\n\n if (this._app) {\n // Inject services once for apps that need them (e.g., HomeApplication for dynamic nav items)\n if (!this._servicesInjected) {\n const appWithServices = this._app as BaseApplication & {\n SetWorkspaceManager?: (manager: WorkspaceStateManager) => void;\n SetSharedService?: (service: SharedService) => void;\n };\n\n if (typeof appWithServices.SetWorkspaceManager === 'function') {\n appWithServices.SetWorkspaceManager(this.workspaceManager);\n }\n if (typeof appWithServices.SetSharedService === 'function') {\n appWithServices.SetSharedService(this.sharedService);\n }\n this._servicesInjected = true;\n }\n\n const items = await this._app.GetNavItems() || [];\n\n // If a newer call started while we were awaiting, our results are stale — bail out\n // so we don't overwrite the newer call's (correct) results.\n if (gen !== this._updateGeneration) {\n return;\n }\n\n // Only show items with Status 'Active' or undefined (default to Active)\n this._cachedNavItems = items.filter(item => !item.Status || item.Status === 'Active');\n\n this._cachedAppColor = this._app.GetColor() || 'var(--mj-brand-primary)';\n } else {\n this._cachedNavItems = [];\n this._cachedAppColor = 'var(--mj-brand-primary)';\n }\n\n // Update active states after nav items change\n const config = this.workspaceManager.GetConfiguration();\n this.updateActiveStates(config);\n this.cdr.markForCheck();\n }\n\n /**\n * Update active state map based on current workspace configuration\n */\n private updateActiveStates(config: WorkspaceConfiguration | null): void {\n this.activeStateMap.clear();\n\n if (!config || !this._app) {\n return;\n }\n\n const activeTab = config.tabs.find(t => t.id === config.activeTabId);\n if (!activeTab || activeTab.applicationId !== this._app.ID) {\n return;\n }\n\n // Compute active state for each nav item once\n for (const item of this._cachedNavItems) {\n const key = this.getItemKey(item);\n const isActive = this.computeIsActive(item, activeTab);\n this.activeStateMap.set(key, isActive);\n }\n }\n\n /**\n * Get unique key for nav item (used for tracking and active state).\n * Prefers RecordID for dynamic items to avoid label collisions.\n */\n private getItemKey(item: NavItem): string {\n return item.RecordID || item.Route || item.Label || '';\n }\n\n /**\n * Check if a nav item is dynamic (generated from recent orphan resources)\n */\n isDynamic(item: NavItem): boolean {\n return (item as DynamicNavItem).isDynamic === true;\n }\n\n /**\n * Compute if nav item is active based on active tab\n */\n private computeIsActive(item: NavItem, activeTab: any): boolean {\n // Check if nav item has a custom matching function (for dynamic items)\n const dynamicItem = item as NavItem & { isActiveMatch?: (tab: unknown) => boolean };\n if (dynamicItem.isActiveMatch && typeof dynamicItem.isActiveMatch === 'function') {\n return dynamicItem.isActiveMatch(activeTab);\n }\n\n const config = activeTab.configuration || {};\n\n // Match by DriverClass (most reliable for Custom resource types — always set correctly)\n if (item.DriverClass && (config['driverClass'] === item.DriverClass || config['resourceTypeDriverClass'] === item.DriverClass)) {\n return true;\n }\n\n // Match by navItemName from config (reliable — set when nav item opens)\n if (config['navItemName'] && config['navItemName'] === item.Label) {\n return true;\n }\n\n // Match by route (for route-based nav items)\n if (item.Route && config['route'] === item.Route) {\n return true;\n }\n\n // NOTE: We intentionally do NOT match by activeTab.title here.\n // Tab titles can be stale (updated asynchronously by DisplayNameChangedEvent\n // from cached components) and cause double-matches where two nav items\n // both appear active. DriverClass and navItemName are sufficient.\n return false;\n }\n\n /**\n * Get cached navigation items (no computation in getter)\n */\n get navItems(): NavItem[] {\n return this._cachedNavItems;\n }\n\n /**\n * Get cached app color (no computation in getter)\n */\n get appColor(): string {\n return this._cachedAppColor;\n }\n\n /**\n * Check if nav item is active (uses cached state from Map)\n */\n isActive(item: NavItem): boolean {\n const key = this.getItemKey(item);\n return this.activeStateMap.get(key) || false;\n }\n\n /**\n * Track function for @for to optimize rendering\n */\n trackByNavItem(_index: number, item: NavItem): string {\n return this.getItemKey(item);\n }\n\n /**\n * Handle nav item click\n */\n onNavClick(item: NavItem, event?: MouseEvent): void {\n this.navItemClick.emit({\n item,\n shiftKey: event?.shiftKey || false\n });\n }\n\n /**\n * Handle dismiss click on a dynamic nav item.\n * Removes from the app's recent stack and refreshes nav items immediately.\n * Stops propagation so the nav click handler doesn't fire.\n */\n onDismiss(item: NavItem, event: MouseEvent): void {\n event.stopPropagation();\n\n // Remove from the app's recent stack directly so we can refresh immediately\n if (this._app) {\n const appWithRemove = this._app as BaseApplication & {\n RemoveDynamicNavItem?: (navItem: NavItem) => void;\n };\n if (typeof appWithRemove.RemoveDynamicNavItem === 'function') {\n appWithRemove.RemoveDynamicNavItem(item);\n this.updateCachedData();\n }\n }\n\n this.navItemDismiss.emit(item);\n }\n\n}\n","<nav class=\"nav-list\" [style.--app-color]=\"appColor\">\n @for (item of navItems; track trackByNavItem($index, item)) {\n <div\n class=\"nav-item\"\n [class.active]=\"isActive(item)\"\n [class.dynamic]=\"isDynamic(item)\"\n [class.no-icon]=\"!item.Icon\"\n (click)=\"onNavClick(item, $event)\">\n @if (item.Icon) {\n <i [class]=\"item.Icon\"></i>\n }\n <span>{{ item.Label }}</span>\n @if (item.Badge) {\n <span class=\"badge\">{{ item.Badge }}</span>\n }\n @if (isDynamic(item)) {\n <button class=\"dismiss-btn\" title=\"Remove\" (click)=\"onDismiss(item, $event)\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n }\n </div>\n }\n</nav>\n"]}
@@ -15,15 +15,31 @@ export interface CachedComponentInfo {
15
15
  lastUsed: Date;
16
16
  createdAt: Date;
17
17
  resourceData: ResourceData;
18
+ savedQueryParams?: Record<string, string>;
19
+ AgentContext?: Record<string, unknown>;
20
+ AgentClientTools?: {
21
+ Name: string;
22
+ Description: string;
23
+ ParameterSchema: Record<string, unknown>;
24
+ Handler: (params: Record<string, unknown>) => Promise<unknown>;
25
+ }[];
18
26
  }
19
27
  /**
20
28
  * Smart component cache manager that preserves component state across tab switches.
21
29
  *
30
+ * ALL cache operations use a consistent identity key: `appId::resourceType::recordId`.
31
+ * This key is the same regardless of whether the component is in Golden Layout (tabbed)
32
+ * mode or Single Resource mode, ensuring components are reusable across both modes.
33
+ *
34
+ * The `attachedToTabId` field is metadata for debugging/display — it is NEVER used
35
+ * as a lookup key. This prevents bugs where multiple resources sharing the same tab ID
36
+ * (e.g., nav items within a single-resource app) interfere with each other's cache state.
37
+ *
22
38
  * Features:
23
- * - Caches components by resource identity (not just tab ID)
39
+ * - Caches components by resource identity (appId + resourceType + recordId)
24
40
  * - Tracks component usage to prevent double-attachment
25
41
  * - Detaches/reattaches DOM elements without destroying Angular components
26
- * - Provides manual cache clearing (no automatic periodic cleanup)
42
+ * - LRU eviction when detached component count exceeds MaxDetachedComponents
27
43
  */
28
44
  export declare class ComponentCacheManager {
29
45
  private appRef;
@@ -36,53 +52,66 @@ export declare class ComponentCacheManager {
36
52
  static MaxDetachedComponents: number;
37
53
  constructor(appRef: ApplicationRef);
38
54
  /**
39
- * Generate a unique cache key from resource identity
55
+ * Generate a unique cache key from resource identity.
56
+ * This is the ONE canonical key format used by ALL cache operations.
40
57
  */
41
58
  private getCacheKey;
42
59
  /**
43
- * Check if a component exists in cache and is available for reuse
60
+ * Check if a component exists in cache and is available for reuse.
44
61
  */
45
62
  hasAvailableComponent(resourceType: string, recordId: string, appId: string): boolean;
46
63
  /**
47
- * Get a cached component if available (not currently attached)
64
+ * Get a cached component if available (not currently attached).
65
+ * Lookup is by resource identity, not tab ID.
48
66
  */
49
67
  getCachedComponent(resourceType: string, recordId: string, appId: string): CachedComponentInfo | null;
50
68
  /**
51
- * Store a component in the cache
69
+ * Store a component in the cache and mark as attached.
52
70
  */
53
71
  cacheComponent(componentRef: ComponentRef<BaseResourceComponent>, wrapperElement: HTMLElement, resourceData: ResourceData, tabId: string): void;
54
72
  /**
55
- * Mark a component as attached to a tab
73
+ * Mark a component as attached. Lookup by resource identity.
56
74
  */
57
75
  markAsAttached(resourceType: string, recordId: string, appId: string, tabId: string): void;
58
76
  /**
59
- * Mark a component as detached (available for reuse)
77
+ * Mark a component as detached (available for reuse). Lookup by resource identity.
78
+ *
79
+ * This is the ONLY way to detach a component. Both single-resource mode and
80
+ * Golden Layout mode use this same method to ensure consistent cache behavior.
81
+ */
82
+ markAsDetached(resourceType: string, recordId: string, appId: string): CachedComponentInfo | null;
83
+ /**
84
+ * Find a cached component by tab ID and detach it.
85
+ * This is a convenience wrapper for callers that only know the tab ID
86
+ * (e.g., Golden Layout tab close events). It resolves the tab ID to
87
+ * resource identity, then delegates to the identity-based markAsDetached.
60
88
  */
61
- markAsDetached(tabId: string): CachedComponentInfo | null;
89
+ findAndDetachByTabId(tabId: string): CachedComponentInfo | null;
62
90
  /**
63
91
  * Evict least-recently-used detached components when over the limit.
64
- * Only evicts components that are not currently attached to a tab.
92
+ * Only evicts components that are not currently attached.
65
93
  */
66
94
  private EvictIfNeeded;
67
95
  /**
68
- * Get component info by tab ID (for finding what's attached to a tab)
96
+ * Get component info by tab ID (for finding what's attached to a tab).
97
+ * Uses linear scan since tabId is metadata, not a key.
69
98
  */
70
99
  getComponentByTabId(tabId: string): CachedComponentInfo | null;
71
100
  /**
72
- * Remove and destroy a specific component from cache
101
+ * Remove and destroy a specific component from cache by resource identity.
73
102
  */
74
103
  destroyComponent(resourceType: string, recordId: string, appId: string): void;
75
104
  /**
76
- * Remove and destroy component by tab ID
105
+ * Remove and destroy component by tab ID (convenience for Golden Layout tab close).
77
106
  */
78
107
  destroyComponentByTabId(tabId: string): void;
79
108
  /**
80
- * Clear the entire cache, destroying all components
81
- * Call this manually when needed (e.g., user logout, app shutdown)
109
+ * Clear the entire cache, destroying all components.
110
+ * Call this on user logout or app shutdown.
82
111
  */
83
112
  clearCache(): void;
84
113
  /**
85
- * Get cache statistics for debugging
114
+ * Get cache statistics for debugging.
86
115
  */
87
116
  getCacheStats(): {
88
117
  total: number;
@@ -1 +1 @@
1
- {"version":3,"file":"component-cache-manager.d.ts","sourceRoot":"","sources":["../../../../../src/lib/shell/components/tabs/component-cache-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAElC,YAAY,EAAE,YAAY,CAAC,qBAAqB,CAAC,CAAC;IAGlD,cAAc,EAAE,WAAW,CAAC;IAG5B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IAGtB,UAAU,EAAE,OAAO,CAAC;IACpB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAG/B,QAAQ,EAAE,IAAI,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAGhB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,qBAAa,qBAAqB;IAUpB,OAAO,CAAC,MAAM;IAT1B,OAAO,CAAC,KAAK,CAA0C;IAEvD;;;;OAIG;IACH,OAAc,qBAAqB,EAAE,MAAM,CAAM;gBAE7B,MAAM,EAAE,cAAc;IAE1C;;OAEG;IACH,OAAO,CAAC,WAAW;IAMnB;;OAEG;IACH,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAMrF;;OAEG;IACH,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAgBrG;;OAEG;IACH,cAAc,CACZ,YAAY,EAAE,YAAY,CAAC,qBAAqB,CAAC,EACjD,cAAc,EAAE,WAAW,EAC3B,YAAY,EAAE,YAAY,EAC1B,KAAK,EAAE,MAAM,GACZ,IAAI;IAuBP;;OAEG;IACH,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAW1F;;OAEG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAmBzD;;;OAGG;IACH,OAAO,CAAC,aAAa;IAerB;;OAEG;IACH,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAO9D;;OAEG;IACH,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAiB7E;;OAEG;IACH,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAmB5C;;;OAGG;IACH,UAAU,IAAI,IAAI;IAYlB;;OAEG;IACH,aAAa,IAAI;QACf,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACrC;CAqBF"}
1
+ {"version":3,"file":"component-cache-manager.d.ts","sourceRoot":"","sources":["../../../../../src/lib/shell/components/tabs/component-cache-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAElC,YAAY,EAAE,YAAY,CAAC,qBAAqB,CAAC,CAAC;IAGlD,cAAc,EAAE,WAAW,CAAC;IAG5B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IAGtB,UAAU,EAAE,OAAO,CAAC;IACpB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAG/B,QAAQ,EAAE,IAAI,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAGhB,YAAY,EAAE,YAAY,CAAC;IAK3B,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAI1C,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAIvC,gBAAgB,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,EAAE,CAAC;CACtK;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,qBAAqB;IAUpB,OAAO,CAAC,MAAM;IAT1B,OAAO,CAAC,KAAK,CAA0C;IAEvD;;;;OAIG;IACH,OAAc,qBAAqB,EAAE,MAAM,CAAM;gBAE7B,MAAM,EAAE,cAAc;IAE1C;;;OAGG;IACH,OAAO,CAAC,WAAW;IAKnB;;OAEG;IACH,qBAAqB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAMrF;;;OAGG;IACH,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAgBrG;;OAEG;IACH,cAAc,CACZ,YAAY,EAAE,YAAY,CAAC,qBAAqB,CAAC,EACjD,cAAc,EAAE,WAAW,EAC3B,YAAY,EAAE,YAAY,EAC1B,KAAK,EAAE,MAAM,GACZ,IAAI;IA6BP;;OAEG;IACH,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAW1F;;;;;OAKG;IACH,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAYjG;;;;;OAKG;IACH,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAU/D;;;OAGG;IACH,OAAO,CAAC,aAAa;IAerB;;;OAGG;IACH,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI;IAO9D;;OAEG;IACH,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAW7E;;OAEG;IACH,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAY5C;;;OAGG;IACH,UAAU,IAAI,IAAI;IAQlB;;OAEG;IACH,aAAa,IAAI;QACf,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACrC;CAqBF"}
@@ -1,11 +1,19 @@
1
1
  /**
2
2
  * Smart component cache manager that preserves component state across tab switches.
3
3
  *
4
+ * ALL cache operations use a consistent identity key: `appId::resourceType::recordId`.
5
+ * This key is the same regardless of whether the component is in Golden Layout (tabbed)
6
+ * mode or Single Resource mode, ensuring components are reusable across both modes.
7
+ *
8
+ * The `attachedToTabId` field is metadata for debugging/display — it is NEVER used
9
+ * as a lookup key. This prevents bugs where multiple resources sharing the same tab ID
10
+ * (e.g., nav items within a single-resource app) interfere with each other's cache state.
11
+ *
4
12
  * Features:
5
- * - Caches components by resource identity (not just tab ID)
13
+ * - Caches components by resource identity (appId + resourceType + recordId)
6
14
  * - Tracks component usage to prevent double-attachment
7
15
  * - Detaches/reattaches DOM elements without destroying Angular components
8
- * - Provides manual cache clearing (no automatic periodic cleanup)
16
+ * - LRU eviction when detached component count exceeds MaxDetachedComponents
9
17
  */
10
18
  export class ComponentCacheManager {
11
19
  appRef;
@@ -20,15 +28,15 @@ export class ComponentCacheManager {
20
28
  this.appRef = appRef;
21
29
  }
22
30
  /**
23
- * Generate a unique cache key from resource identity
31
+ * Generate a unique cache key from resource identity.
32
+ * This is the ONE canonical key format used by ALL cache operations.
24
33
  */
25
34
  getCacheKey(resourceType, recordId, appId) {
26
- // Normalize empty/null recordId to ensure consistent matching
27
35
  const normalizedRecordId = recordId || '__no_record__';
28
36
  return `${appId}::${resourceType}::${normalizedRecordId}`;
29
37
  }
30
38
  /**
31
- * Check if a component exists in cache and is available for reuse
39
+ * Check if a component exists in cache and is available for reuse.
32
40
  */
33
41
  hasAvailableComponent(resourceType, recordId, appId) {
34
42
  const key = this.getCacheKey(resourceType, recordId, appId);
@@ -36,7 +44,8 @@ export class ComponentCacheManager {
36
44
  return info !== undefined && !info.isAttached;
37
45
  }
38
46
  /**
39
- * Get a cached component if available (not currently attached)
47
+ * Get a cached component if available (not currently attached).
48
+ * Lookup is by resource identity, not tab ID.
40
49
  */
41
50
  getCachedComponent(resourceType, recordId, appId) {
42
51
  const key = this.getCacheKey(resourceType, recordId, appId);
@@ -51,14 +60,20 @@ export class ComponentCacheManager {
51
60
  return info;
52
61
  }
53
62
  /**
54
- * Store a component in the cache
63
+ * Store a component in the cache and mark as attached.
55
64
  */
56
65
  cacheComponent(componentRef, wrapperElement, resourceData, tabId) {
57
- const key = this.getCacheKey(resourceData.ResourceType, resourceData.ResourceRecordID || '', resourceData.Configuration?.applicationId || '');
66
+ // Use driverClass (the actual component class name) as the resourceType for the cache key,
67
+ // NOT resourceData.ResourceType (which is often just "Custom" for dashboard resources).
68
+ // This must match the lookup key used in getCachedComponent/markAsAttached/markAsDetached.
69
+ const resolvedResourceType = resourceData.Configuration?.resourceTypeDriverClass
70
+ || resourceData.Configuration?.driverClass
71
+ || resourceData.ResourceType;
72
+ const key = this.getCacheKey(resolvedResourceType, resourceData.ResourceRecordID || '', resourceData.Configuration?.applicationId || '');
58
73
  const info = {
59
74
  componentRef,
60
75
  wrapperElement,
61
- resourceType: resourceData.ResourceType,
76
+ resourceType: resolvedResourceType,
62
77
  resourceRecordId: resourceData.ResourceRecordID || '',
63
78
  applicationId: resourceData.Configuration?.applicationId || '',
64
79
  isAttached: true,
@@ -70,7 +85,7 @@ export class ComponentCacheManager {
70
85
  this.cache.set(key, info);
71
86
  }
72
87
  /**
73
- * Mark a component as attached to a tab
88
+ * Mark a component as attached. Lookup by resource identity.
74
89
  */
75
90
  markAsAttached(resourceType, recordId, appId, tabId) {
76
91
  const key = this.getCacheKey(resourceType, recordId, appId);
@@ -82,25 +97,39 @@ export class ComponentCacheManager {
82
97
  }
83
98
  }
84
99
  /**
85
- * Mark a component as detached (available for reuse)
100
+ * Mark a component as detached (available for reuse). Lookup by resource identity.
101
+ *
102
+ * This is the ONLY way to detach a component. Both single-resource mode and
103
+ * Golden Layout mode use this same method to ensure consistent cache behavior.
86
104
  */
87
- markAsDetached(tabId) {
88
- // Find component by tab ID
89
- const entry = Array.from(this.cache.entries())
90
- .find(([_, info]) => info.attachedToTabId === tabId);
91
- if (!entry) {
105
+ markAsDetached(resourceType, recordId, appId) {
106
+ const key = this.getCacheKey(resourceType, recordId, appId);
107
+ const info = this.cache.get(key);
108
+ if (!info)
92
109
  return null;
93
- }
94
- const [key, info] = entry;
95
110
  info.isAttached = false;
96
111
  info.attachedToTabId = null;
97
112
  info.lastUsed = new Date();
98
113
  this.EvictIfNeeded();
99
114
  return info;
100
115
  }
116
+ /**
117
+ * Find a cached component by tab ID and detach it.
118
+ * This is a convenience wrapper for callers that only know the tab ID
119
+ * (e.g., Golden Layout tab close events). It resolves the tab ID to
120
+ * resource identity, then delegates to the identity-based markAsDetached.
121
+ */
122
+ findAndDetachByTabId(tabId) {
123
+ const entry = Array.from(this.cache.entries())
124
+ .find(([_, info]) => info.attachedToTabId === tabId);
125
+ if (!entry)
126
+ return null;
127
+ const [_, info] = entry;
128
+ return this.markAsDetached(info.resourceType, info.resourceRecordId, info.applicationId);
129
+ }
101
130
  /**
102
131
  * Evict least-recently-used detached components when over the limit.
103
- * Only evicts components that are not currently attached to a tab.
132
+ * Only evicts components that are not currently attached.
104
133
  */
105
134
  EvictIfNeeded() {
106
135
  if (ComponentCacheManager.MaxDetachedComponents <= 0)
@@ -116,7 +145,8 @@ export class ComponentCacheManager {
116
145
  }
117
146
  }
118
147
  /**
119
- * Get component info by tab ID (for finding what's attached to a tab)
148
+ * Get component info by tab ID (for finding what's attached to a tab).
149
+ * Uses linear scan since tabId is metadata, not a key.
120
150
  */
121
151
  getComponentByTabId(tabId) {
122
152
  const entry = Array.from(this.cache.entries())
@@ -124,51 +154,43 @@ export class ComponentCacheManager {
124
154
  return entry ? entry[1] : null;
125
155
  }
126
156
  /**
127
- * Remove and destroy a specific component from cache
157
+ * Remove and destroy a specific component from cache by resource identity.
128
158
  */
129
159
  destroyComponent(resourceType, recordId, appId) {
130
160
  const key = this.getCacheKey(resourceType, recordId, appId);
131
161
  const info = this.cache.get(key);
132
- if (!info) {
162
+ if (!info)
133
163
  return;
134
- }
135
- // Destroy Angular component
136
164
  this.appRef.detachView(info.componentRef.hostView);
137
165
  info.componentRef.destroy();
138
- // Remove from cache
139
166
  this.cache.delete(key);
140
167
  }
141
168
  /**
142
- * Remove and destroy component by tab ID
169
+ * Remove and destroy component by tab ID (convenience for Golden Layout tab close).
143
170
  */
144
171
  destroyComponentByTabId(tabId) {
145
172
  const entry = Array.from(this.cache.entries())
146
173
  .find(([_, info]) => info.attachedToTabId === tabId);
147
- if (!entry) {
174
+ if (!entry)
148
175
  return;
149
- }
150
176
  const [key, info] = entry;
151
- // Destroy Angular component
152
177
  this.appRef.detachView(info.componentRef.hostView);
153
178
  info.componentRef.destroy();
154
- // Remove from cache
155
179
  this.cache.delete(key);
156
180
  }
157
181
  /**
158
- * Clear the entire cache, destroying all components
159
- * Call this manually when needed (e.g., user logout, app shutdown)
182
+ * Clear the entire cache, destroying all components.
183
+ * Call this on user logout or app shutdown.
160
184
  */
161
185
  clearCache() {
162
- // Destroy all components
163
186
  this.cache.forEach(info => {
164
187
  this.appRef.detachView(info.componentRef.hostView);
165
188
  info.componentRef.destroy();
166
189
  });
167
- // Clear the map
168
190
  this.cache.clear();
169
191
  }
170
192
  /**
171
- * Get cache statistics for debugging
193
+ * Get cache statistics for debugging.
172
194
  */
173
195
  getCacheStats() {
174
196
  const stats = {
@@ -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 (for matching)\n resourceType: string;\n resourceRecordId: string;\n applicationId: string;\n\n // Usage tracking\n isAttached: boolean; // Currently attached to a tab?\n attachedToTabId: string | null; // Which tab is it attached to?\n\n // Lifecycle tracking\n lastUsed: Date;\n createdAt: Date;\n\n // Resource data snapshot (for comparison)\n resourceData: ResourceData;\n}\n\n/**\n * Smart component cache manager that preserves component state across tab switches.\n *\n * Features:\n * - Caches components by resource identity (not just tab ID)\n * - Tracks component usage to prevent double-attachment\n * - Detaches/reattaches DOM elements without destroying Angular components\n * - Provides manual cache clearing (no automatic periodic cleanup)\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 */\n private getCacheKey(resourceType: string, recordId: string, appId: string): string {\n // Normalize empty/null recordId to ensure consistent matching\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 */\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\n */\n cacheComponent(\n componentRef: ComponentRef<BaseResourceComponent>,\n wrapperElement: HTMLElement,\n resourceData: ResourceData,\n tabId: string\n ): void {\n const key = this.getCacheKey(\n resourceData.ResourceType,\n resourceData.ResourceRecordID || '',\n resourceData.Configuration?.applicationId || ''\n );\n\n const info: CachedComponentInfo = {\n componentRef,\n wrapperElement,\n resourceType: resourceData.ResourceType,\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 to a tab\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)\n */\n markAsDetached(tabId: string): CachedComponentInfo | null {\n // Find component by tab ID\n const entry = Array.from(this.cache.entries())\n .find(([_, info]) => info.attachedToTabId === tabId);\n\n if (!entry) {\n return null;\n }\n\n const [key, info] = entry;\n info.isAttached = false;\n info.attachedToTabId = null;\n info.lastUsed = new Date();\n\n this.EvictIfNeeded();\n\n return info;\n }\n\n /**\n * Evict least-recently-used detached components when over the limit.\n * Only evicts components that are not currently attached to a tab.\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 */\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\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) {\n return;\n }\n\n // Destroy Angular component\n this.appRef.detachView(info.componentRef.hostView);\n info.componentRef.destroy();\n\n // Remove from cache\n this.cache.delete(key);\n\n }\n\n /**\n * Remove and destroy component by tab ID\n */\n destroyComponentByTabId(tabId: string): void {\n const entry = Array.from(this.cache.entries())\n .find(([_, info]) => info.attachedToTabId === tabId);\n\n if (!entry) {\n return;\n }\n\n const [key, info] = entry;\n\n // Destroy Angular component\n this.appRef.detachView(info.componentRef.hostView);\n info.componentRef.destroy();\n\n // Remove from cache\n this.cache.delete(key);\n\n }\n\n /**\n * Clear the entire cache, destroying all components\n * Call this manually when needed (e.g., user logout, app shutdown)\n */\n clearCache(): void {\n\n // Destroy all components\n this.cache.forEach(info => {\n this.appRef.detachView(info.componentRef.hostView);\n info.componentRef.destroy();\n });\n\n // Clear the map\n this.cache.clear();\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 // 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 * 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"]}
@@ -42,6 +42,8 @@ export declare class TabContainerComponent implements OnInit, OnDestroy, AfterVi
42
42
  private cacheManager;
43
43
  useSingleResourceMode: boolean;
44
44
  private singleResourceComponentRef;
45
+ /** Cache identity of the current single-resource component for detachment */
46
+ private singleResourceCacheIdentity;
45
47
  private previousTabBarVisible;
46
48
  private currentSingleResourceSignature;
47
49
  private isCreatingInitialTabs;
@@ -75,6 +77,37 @@ export declare class TabContainerComponent implements OnInit, OnDestroy, AfterVi
75
77
  /**
76
78
  * Clean up single-resource mode component
77
79
  */
80
+ /**
81
+ * Detaches the current single-resource component from the DOM and marks it as
82
+ * available for reuse in the component cache.
83
+ *
84
+ * ╔══════════════════════════════════════════════════════════════════════════╗
85
+ * ║ ⚠️ DO NOT DESTROY THE COMPONENT HERE — INTENTIONAL DESIGN CHOICE ⚠️ ║
86
+ * ║ ║
87
+ * ║ The component is DETACHED from the DOM, NOT destroyed. It stays alive ║
88
+ * ║ in the ComponentCacheManager with its full Angular state preserved ║
89
+ * ║ (properties, subscriptions, loaded data, scroll position, etc). ║
90
+ * ║ ║
91
+ * ║ When the user returns to this tab, the cached component is reattached ║
92
+ * ║ instantly — no data reload, no API calls, no flash of empty content. ║
93
+ * ║ ║
94
+ * ║ Destroying components here "for memory optimization" is a net ║
95
+ * ║ NEGATIVE: the reload on return is far more expensive (DB queries, ║
96
+ * ║ API calls, re-rendering) than keeping the component in memory. ║
97
+ * ║ The LRU eviction in ComponentCacheManager handles memory limits — ║
98
+ * ║ when MaxDetachedComponents is exceeded, the LEAST recently used ║
99
+ * ║ components are evicted automatically. ║
100
+ * ║ ║
101
+ * ║ If you think memory is a problem, adjust MaxDetachedComponents ║
102
+ * ║ instead of destroying components here. ║
103
+ * ╚══════════════════════════════════════════════════════════════════════════╝
104
+ */
105
+ /**
106
+ * Save the currently displayed component's queryParams to its cache entry.
107
+ * Called on every config change so the cache entry always has the latest queryParams,
108
+ * even after the tab config is overwritten by a new nav item.
109
+ */
110
+ private saveCurrentComponentQueryParams;
78
111
  private cleanupSingleResourceComponent;
79
112
  /**
80
113
  * Generate a signature for tab content to detect when content changes