@memberjunction/ng-dashboards 6.2.0-edge.3 → 6.2.0-edge.4

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 (177) hide show
  1. package/dist/AI/components/analytics/realtime/realtime-transcripts-data.d.ts +4 -3
  2. package/dist/AI/components/analytics/realtime/realtime-transcripts-data.d.ts.map +1 -1
  3. package/dist/AI/components/analytics/realtime/realtime-transcripts-data.js +8 -5
  4. package/dist/AI/components/analytics/realtime/realtime-transcripts-data.js.map +1 -1
  5. package/dist/AI/components/duplicates/compare-value.component.d.ts +67 -0
  6. package/dist/AI/components/duplicates/compare-value.component.d.ts.map +1 -0
  7. package/dist/AI/components/duplicates/compare-value.component.js +168 -0
  8. package/dist/AI/components/duplicates/compare-value.component.js.map +1 -0
  9. package/dist/AI/components/duplicates/duplicate-detection-resource.component.d.ts +49 -3
  10. package/dist/AI/components/duplicates/duplicate-detection-resource.component.d.ts.map +1 -1
  11. package/dist/AI/components/duplicates/duplicate-detection-resource.component.js +287 -169
  12. package/dist/AI/components/duplicates/duplicate-detection-resource.component.js.map +1 -1
  13. package/dist/AI/components/duplicates/duplicate-detection-run-scope.d.ts +77 -0
  14. package/dist/AI/components/duplicates/duplicate-detection-run-scope.d.ts.map +1 -0
  15. package/dist/AI/components/duplicates/duplicate-detection-run-scope.js +116 -0
  16. package/dist/AI/components/duplicates/duplicate-detection-run-scope.js.map +1 -0
  17. package/dist/AI/components/prompts/model-prompt-priority-matrix.component.d.ts +1 -0
  18. package/dist/AI/components/prompts/model-prompt-priority-matrix.component.d.ts.map +1 -1
  19. package/dist/AI/components/prompts/model-prompt-priority-matrix.component.js +16 -4
  20. package/dist/AI/components/prompts/model-prompt-priority-matrix.component.js.map +1 -1
  21. package/dist/AI/components/tags/tags-resource.component.d.ts +2 -0
  22. package/dist/AI/components/tags/tags-resource.component.d.ts.map +1 -1
  23. package/dist/AI/components/tags/tags-resource.component.js +12 -6
  24. package/dist/AI/components/tags/tags-resource.component.js.map +1 -1
  25. package/dist/AI/components/vectors/vector-index-auto.d.ts +29 -0
  26. package/dist/AI/components/vectors/vector-index-auto.d.ts.map +1 -0
  27. package/dist/AI/components/vectors/vector-index-auto.js +59 -0
  28. package/dist/AI/components/vectors/vector-index-auto.js.map +1 -0
  29. package/dist/AI/components/vectors/vector-management-resource.component.d.ts +9 -0
  30. package/dist/AI/components/vectors/vector-management-resource.component.d.ts.map +1 -1
  31. package/dist/AI/components/vectors/vector-management-resource.component.js +65 -20
  32. package/dist/AI/components/vectors/vector-management-resource.component.js.map +1 -1
  33. package/dist/Actions/components/actions-overview.component.d.ts.map +1 -1
  34. package/dist/Actions/components/actions-overview.component.js +7 -3
  35. package/dist/Actions/components/actions-overview.component.js.map +1 -1
  36. package/dist/Actions/components/categories-list-view.component.d.ts.map +1 -1
  37. package/dist/Actions/components/categories-list-view.component.js +3 -2
  38. package/dist/Actions/components/categories-list-view.component.js.map +1 -1
  39. package/dist/ComponentStudio/component-studio-dashboard.component.d.ts.map +1 -1
  40. package/dist/ComponentStudio/component-studio-dashboard.component.js +1 -0
  41. package/dist/ComponentStudio/component-studio-dashboard.component.js.map +1 -1
  42. package/dist/ComponentStudio/components/ai-assistant/ai-assistant-panel.component.d.ts +0 -3
  43. package/dist/ComponentStudio/components/ai-assistant/ai-assistant-panel.component.d.ts.map +1 -1
  44. package/dist/ComponentStudio/components/ai-assistant/ai-assistant-panel.component.js +5 -14
  45. package/dist/ComponentStudio/components/ai-assistant/ai-assistant-panel.component.js.map +1 -1
  46. package/dist/ComponentStudio/components/artifact-load-dialog.component.d.ts.map +1 -1
  47. package/dist/ComponentStudio/components/artifact-load-dialog.component.js +1 -0
  48. package/dist/ComponentStudio/components/artifact-load-dialog.component.js.map +1 -1
  49. package/dist/ComponentStudio/components/artifact-selection-dialog.component.d.ts.map +1 -1
  50. package/dist/ComponentStudio/components/artifact-selection-dialog.component.js +1 -0
  51. package/dist/ComponentStudio/components/artifact-selection-dialog.component.js.map +1 -1
  52. package/dist/Credentials/components/credentials-list-resource.component.d.ts +2 -0
  53. package/dist/Credentials/components/credentials-list-resource.component.d.ts.map +1 -1
  54. package/dist/Credentials/components/credentials-list-resource.component.js +12 -10
  55. package/dist/Credentials/components/credentials-list-resource.component.js.map +1 -1
  56. package/dist/DashboardBrowser/dashboard-browser-agent-context.d.ts +14 -43
  57. package/dist/DashboardBrowser/dashboard-browser-agent-context.d.ts.map +1 -1
  58. package/dist/DashboardBrowser/dashboard-browser-agent-context.js +6 -29
  59. package/dist/DashboardBrowser/dashboard-browser-agent-context.js.map +1 -1
  60. package/dist/DashboardBrowser/dashboard-browser-resource.component.d.ts +174 -321
  61. package/dist/DashboardBrowser/dashboard-browser-resource.component.d.ts.map +1 -1
  62. package/dist/DashboardBrowser/dashboard-browser-resource.component.js +522 -1195
  63. package/dist/DashboardBrowser/dashboard-browser-resource.component.js.map +1 -1
  64. package/dist/DashboardBrowser/dashboard-library-filter.d.ts +116 -0
  65. package/dist/DashboardBrowser/dashboard-library-filter.d.ts.map +1 -0
  66. package/dist/DashboardBrowser/dashboard-library-filter.js +277 -0
  67. package/dist/DashboardBrowser/dashboard-library-filter.js.map +1 -0
  68. package/dist/DashboardBrowser/dashboard-share-dialog.component.d.ts +1 -1
  69. package/dist/DashboardBrowser/dashboard-share-dialog.component.js.map +1 -1
  70. package/dist/DashboardBrowser/dashboards-agent-tools.d.ts +26 -0
  71. package/dist/DashboardBrowser/dashboards-agent-tools.d.ts.map +1 -0
  72. package/dist/DashboardBrowser/dashboards-agent-tools.js +32 -0
  73. package/dist/DashboardBrowser/dashboards-agent-tools.js.map +1 -0
  74. package/dist/DashboardBrowser/dashboards-categories-resource.component.d.ts +58 -0
  75. package/dist/DashboardBrowser/dashboards-categories-resource.component.d.ts.map +1 -0
  76. package/dist/DashboardBrowser/dashboards-categories-resource.component.js +391 -0
  77. package/dist/DashboardBrowser/dashboards-categories-resource.component.js.map +1 -0
  78. package/dist/DashboardBrowser/dashboards-categories.helpers.d.ts +37 -0
  79. package/dist/DashboardBrowser/dashboards-categories.helpers.d.ts.map +1 -0
  80. package/dist/DashboardBrowser/dashboards-categories.helpers.js +68 -0
  81. package/dist/DashboardBrowser/dashboards-categories.helpers.js.map +1 -0
  82. package/dist/DashboardEditor/dashboard-editor.component.d.ts +295 -0
  83. package/dist/DashboardEditor/dashboard-editor.component.d.ts.map +1 -0
  84. package/dist/DashboardEditor/dashboard-editor.component.js +1022 -0
  85. package/dist/DashboardEditor/dashboard-editor.component.js.map +1 -0
  86. package/dist/DashboardEditor/dashboard-editor.types.d.ts +23 -0
  87. package/dist/DashboardEditor/dashboard-editor.types.d.ts.map +1 -0
  88. package/dist/DashboardEditor/dashboard-editor.types.js +3 -0
  89. package/dist/DashboardEditor/dashboard-editor.types.js.map +1 -0
  90. package/dist/DashboardEditor/dashboard-viewer-factory.d.ts +11 -0
  91. package/dist/DashboardEditor/dashboard-viewer-factory.d.ts.map +1 -0
  92. package/dist/DashboardEditor/dashboard-viewer-factory.js +17 -0
  93. package/dist/DashboardEditor/dashboard-viewer-factory.js.map +1 -0
  94. package/dist/DataExplorer/data-explorer-dashboard.component.d.ts +26 -0
  95. package/dist/DataExplorer/data-explorer-dashboard.component.d.ts.map +1 -1
  96. package/dist/DataExplorer/data-explorer-dashboard.component.js +302 -219
  97. package/dist/DataExplorer/data-explorer-dashboard.component.js.map +1 -1
  98. package/dist/DataExplorer/services/explorer-state.service.d.ts +1 -1
  99. package/dist/DataExplorer/services/explorer-state.service.js +2 -5
  100. package/dist/DataExplorer/services/explorer-state.service.js.map +1 -1
  101. package/dist/FormBuilder/form-builder-resource.component.d.ts +0 -14
  102. package/dist/FormBuilder/form-builder-resource.component.d.ts.map +1 -1
  103. package/dist/FormBuilder/form-builder-resource.component.js +7 -29
  104. package/dist/FormBuilder/form-builder-resource.component.js.map +1 -1
  105. package/dist/Home/home-agent-context.d.ts +29 -10
  106. package/dist/Home/home-agent-context.d.ts.map +1 -1
  107. package/dist/Home/home-agent-context.js +50 -24
  108. package/dist/Home/home-agent-context.js.map +1 -1
  109. package/dist/Home/home-dashboard-switcher.component.d.ts +95 -0
  110. package/dist/Home/home-dashboard-switcher.component.d.ts.map +1 -0
  111. package/dist/Home/home-dashboard-switcher.component.js +442 -0
  112. package/dist/Home/home-dashboard-switcher.component.js.map +1 -0
  113. package/dist/Home/home-dashboard.component.d.ts +330 -17
  114. package/dist/Home/home-dashboard.component.d.ts.map +1 -1
  115. package/dist/Home/home-dashboard.component.js +1702 -1014
  116. package/dist/Home/home-dashboard.component.js.map +1 -1
  117. package/dist/Home/home-menus.d.ts +38 -0
  118. package/dist/Home/home-menus.d.ts.map +1 -0
  119. package/dist/Home/home-menus.js +53 -0
  120. package/dist/Home/home-menus.js.map +1 -0
  121. package/dist/Home/home-pinned-dashboards.d.ts +33 -0
  122. package/dist/Home/home-pinned-dashboards.d.ts.map +1 -0
  123. package/dist/Home/home-pinned-dashboards.js +76 -0
  124. package/dist/Home/home-pinned-dashboards.js.map +1 -0
  125. package/dist/PredictiveStudio/components/ps-production.component.d.ts +5 -1
  126. package/dist/PredictiveStudio/components/ps-production.component.d.ts.map +1 -1
  127. package/dist/PredictiveStudio/components/ps-production.component.js +64 -41
  128. package/dist/PredictiveStudio/components/ps-production.component.js.map +1 -1
  129. package/dist/QueryBrowser/query-browser-resource.component.d.ts.map +1 -1
  130. package/dist/QueryBrowser/query-browser-resource.component.js +3 -2
  131. package/dist/QueryBrowser/query-browser-resource.component.js.map +1 -1
  132. package/dist/RealtimeRecordings/realtime-recordings-dashboard.component.js +2 -2
  133. package/dist/RealtimeRecordings/realtime-recordings-dashboard.component.js.map +1 -1
  134. package/dist/TelephonyOperations/telephony-operations-dashboard.component.d.ts +1 -0
  135. package/dist/TelephonyOperations/telephony-operations-dashboard.component.d.ts.map +1 -1
  136. package/dist/TelephonyOperations/telephony-operations-dashboard.component.js +7 -0
  137. package/dist/TelephonyOperations/telephony-operations-dashboard.component.js.map +1 -1
  138. package/dist/Testing/components/testing-explorer.component.d.ts.map +1 -1
  139. package/dist/Testing/components/testing-explorer.component.js +7 -7
  140. package/dist/Testing/components/testing-explorer.component.js.map +1 -1
  141. package/dist/ai-dashboards.module.d.ts +21 -20
  142. package/dist/ai-dashboards.module.d.ts.map +1 -1
  143. package/dist/ai-dashboards.module.js +4 -1
  144. package/dist/ai-dashboards.module.js.map +1 -1
  145. package/dist/core-dashboards.module.d.ts +69 -62
  146. package/dist/core-dashboards.module.d.ts.map +1 -1
  147. package/dist/core-dashboards.module.js +22 -2
  148. package/dist/core-dashboards.module.js.map +1 -1
  149. package/dist/data-explorer-dashboards.module.d.ts +1 -1
  150. package/dist/data-explorer-dashboards.module.d.ts.map +1 -1
  151. package/dist/data-explorer-dashboards.module.js +3 -1
  152. package/dist/data-explorer-dashboards.module.js.map +1 -1
  153. package/dist/public-api.d.ts +2 -0
  154. package/dist/public-api.d.ts.map +1 -1
  155. package/dist/public-api.js +3 -0
  156. package/dist/public-api.js.map +1 -1
  157. package/dist/shared/dashboard-library-changes.d.ts +23 -0
  158. package/dist/shared/dashboard-library-changes.d.ts.map +1 -0
  159. package/dist/shared/dashboard-library-changes.js +26 -0
  160. package/dist/shared/dashboard-library-changes.js.map +1 -0
  161. package/dist/shared/dashboard-owner-names.d.ts +17 -0
  162. package/dist/shared/dashboard-owner-names.d.ts.map +1 -0
  163. package/dist/shared/dashboard-owner-names.js +65 -0
  164. package/dist/shared/dashboard-owner-names.js.map +1 -0
  165. package/dist/shared/dashboard-recents.d.ts +34 -0
  166. package/dist/shared/dashboard-recents.d.ts.map +1 -0
  167. package/dist/shared/dashboard-recents.js +87 -0
  168. package/dist/shared/dashboard-recents.js.map +1 -0
  169. package/dist/shared/dashboards-app-install.d.ts +59 -0
  170. package/dist/shared/dashboards-app-install.d.ts.map +1 -0
  171. package/dist/shared/dashboards-app-install.js +97 -0
  172. package/dist/shared/dashboards-app-install.js.map +1 -0
  173. package/dist/shared/dashboards-app.helpers.d.ts +42 -0
  174. package/dist/shared/dashboards-app.helpers.d.ts.map +1 -0
  175. package/dist/shared/dashboards-app.helpers.js +143 -0
  176. package/dist/shared/dashboards-app.helpers.js.map +1 -0
  177. package/package.json +69 -68
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboards-categories-resource.component.d.ts","sourceRoot":"","sources":["../../src/DashboardBrowser/dashboards-categories-resource.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,iBAAiB,EAAa,SAAS,EAAE,MAAM,EAAU,MAAM,eAAe,CAAC;AAIjH,OAAO,EAAmB,yBAAyB,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AACzG,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AASlE,OAAO,EAIL,WAAW,EAEZ,MAAM,iCAAiC,CAAC;;AAEzC;;;;;GAKG;AACH,qBAuFa,qCAAsC,SAAQ,qBAAsB,YAAW,MAAM,EAAE,SAAS;IAoB/F,OAAO,CAAC,GAAG;IAnBvB,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,aAAa,CAAiC;IACtD,OAAO,CAAC,cAAc,CAA4B;IAElD,6CAA6C;IACtC,SAAS,UAAQ;IAExB,yDAAyD;IAClD,QAAQ,UAAS;IAExB,4DAA4D;IACrD,IAAI,EAAE,WAAW,EAAE,CAAM;IAEhC,8CAA8C;IACvC,OAAO,SAAM;IAEpB,iDAAiD;IACjD,SAAgB,aAAa,OAA4B;gBAErC,GAAG,EAAE,iBAAiB;IAI1C,QAAQ,IAAI,IAAI;IAWhB,WAAW,IAAI,IAAI;IAIb,sBAAsB,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAI5D,oBAAoB,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAIhE,8EAA8E;IAC9E,IAAW,SAAS,IAAI,OAAO,CAE9B;IAED,2EAA2E;IACpE,OAAO,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM;IAIxC,uGAAuG;IAC1F,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IA4BpC,wEAAwE;IAC3D,MAAM,CAAC,QAAQ,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BvE,oEAAoE;IACvD,aAAa,CAAC,QAAQ,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC;YAIhE,IAAI;IAclB;;;OAGG;YACW,MAAM;IAcpB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,WAAW;YAQL,aAAa;IAe3B,OAAO,CAAC,SAAS;IAmBjB,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,eAAe;yCAvNZ,qCAAqC;2CAArC,qCAAqC;CA+OjD;AAMD,wBAAgB,gCAAgC,IAAI,IAAI,CAEvD"}
@@ -0,0 +1,391 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
8
+ import { takeUntil } from 'rxjs/operators';
9
+ import { LogError } from '@memberjunction/core';
10
+ import { RegisterClass } from '@memberjunction/global';
11
+ import { DashboardEngine } from '@memberjunction/core-entities';
12
+ import { BaseResourceComponent } from '@memberjunction/ng-shared';
13
+ import { ApplicationManager } from '@memberjunction/ng-base-application';
14
+ import { MJNotificationService } from '@memberjunction/ng-notifications';
15
+ import { MJConfirmService } from '@memberjunction/ng-ui-components';
16
+ import { AGENT_CONTEXT_NAME_LIST_CAP } from '../shared/agent-tool-validation';
17
+ import { DASHBOARDS_LIBRARY_NAV_ITEM, FindDashboardsApp } from '../shared/dashboards-app.helpers';
18
+ import { ObserveDashboardLibraryChanges } from '../shared/dashboard-library-changes';
19
+ import { LocationQueryParams } from './dashboard-library-filter';
20
+ import { ResolveByIdOrName } from './dashboards-agent-tools';
21
+ import { BuildCategoryRows, BuildEffectiveCategoryMap, CATEGORY_NAME_MAX_LENGTH, CategoryRowSummary, } from './dashboards-categories.helpers';
22
+ import * as i0 from "@angular/core";
23
+ import * as i1 from "@angular/forms";
24
+ import * as i2 from "@memberjunction/ng-ui-components";
25
+ import * as i3 from "@memberjunction/ng-shared-generic";
26
+ const _forTrack0 = ($index, $item) => $item.Category.ID;
27
+ function DashboardsCategoriesResourceComponent_Conditional_8_Template(rf, ctx) { if (rf & 1) {
28
+ i0.ɵɵelement(0, "mj-loading", 5);
29
+ } }
30
+ function DashboardsCategoriesResourceComponent_Conditional_9_For_1_Conditional_9_Template(rf, ctx) { if (rf & 1) {
31
+ const _r4 = i0.ɵɵgetCurrentView();
32
+ i0.ɵɵelementStart(0, "button", 13);
33
+ i0.ɵɵlistener("click", function DashboardsCategoriesResourceComponent_Conditional_9_For_1_Conditional_9_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r4); const row_r2 = i0.ɵɵnextContext().$implicit; const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.Delete(row_r2.Category)); });
34
+ i0.ɵɵelement(1, "i", 14);
35
+ i0.ɵɵtext(2, " Delete ");
36
+ i0.ɵɵelementEnd();
37
+ } if (rf & 2) {
38
+ const row_r2 = i0.ɵɵnextContext().$implicit;
39
+ const ctx_r2 = i0.ɵɵnextContext(2);
40
+ i0.ɵɵproperty("ariaLabel", "Delete " + row_r2.Category.Name)("disabled", ctx_r2.IsSaving);
41
+ } }
42
+ function DashboardsCategoriesResourceComponent_Conditional_9_For_1_Template(rf, ctx) { if (rf & 1) {
43
+ const _r1 = i0.ɵɵgetCurrentView();
44
+ i0.ɵɵelementStart(0, "div", 8);
45
+ i0.ɵɵelement(1, "i", 9);
46
+ i0.ɵɵelementStart(2, "span", 10)(3, "b");
47
+ i0.ɵɵtext(4);
48
+ i0.ɵɵelementEnd();
49
+ i0.ɵɵelementStart(5, "small");
50
+ i0.ɵɵtext(6);
51
+ i0.ɵɵelementEnd()();
52
+ i0.ɵɵelementStart(7, "button", 11);
53
+ i0.ɵɵlistener("click", function DashboardsCategoriesResourceComponent_Conditional_9_For_1_Template_button_click_7_listener() { const row_r2 = i0.ɵɵrestoreView(_r1).$implicit; const ctx_r2 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r2.OpenInLibrary(row_r2.Category)); });
54
+ i0.ɵɵtext(8, "Open");
55
+ i0.ɵɵelementEnd();
56
+ i0.ɵɵconditionalCreate(9, DashboardsCategoriesResourceComponent_Conditional_9_For_1_Conditional_9_Template, 3, 2, "button", 12);
57
+ i0.ɵɵelementEnd();
58
+ } if (rf & 2) {
59
+ const row_r2 = ctx.$implicit;
60
+ const ctx_r2 = i0.ɵɵnextContext(2);
61
+ i0.ɵɵstyleProp("padding-left", 14 + row_r2.Depth * 28, "px");
62
+ i0.ɵɵadvance(4);
63
+ i0.ɵɵtextInterpolate(row_r2.Category.Name);
64
+ i0.ɵɵadvance(2);
65
+ i0.ɵɵtextInterpolate(ctx_r2.Summary(row_r2));
66
+ i0.ɵɵadvance();
67
+ i0.ɵɵproperty("ariaLabel", "Open " + row_r2.Category.Name + " in the Library");
68
+ i0.ɵɵadvance(2);
69
+ i0.ɵɵconditional(row_r2.CanDelete ? 9 : -1);
70
+ } }
71
+ function DashboardsCategoriesResourceComponent_Conditional_9_ForEmpty_2_Template(rf, ctx) { if (rf & 1) {
72
+ i0.ɵɵelement(0, "mj-empty-state", 7);
73
+ } }
74
+ function DashboardsCategoriesResourceComponent_Conditional_9_Template(rf, ctx) { if (rf & 1) {
75
+ i0.ɵɵrepeaterCreate(0, DashboardsCategoriesResourceComponent_Conditional_9_For_1_Template, 10, 6, "div", 6, _forTrack0, false, DashboardsCategoriesResourceComponent_Conditional_9_ForEmpty_2_Template, 1, 0, "mj-empty-state", 7);
76
+ } if (rf & 2) {
77
+ const ctx_r2 = i0.ɵɵnextContext();
78
+ i0.ɵɵrepeater(ctx_r2.Rows);
79
+ } }
80
+ /**
81
+ * Categories page of the Dashboards app: the user's category tree with dashboard and sub-category
82
+ * counts, a New category box, Delete, after a confirm, for an empty category the user owns, and Open,
83
+ * which shows the category on the Library page. The agent tools are read-only; see the SAFETY
84
+ * BOUNDARY in the agent section below.
85
+ */
86
+ let DashboardsCategoriesResourceComponent = class DashboardsCategoriesResourceComponent extends BaseResourceComponent {
87
+ cdr;
88
+ appManager = inject(ApplicationManager);
89
+ notifications = inject(MJNotificationService);
90
+ confirmService = inject(MJConfirmService);
91
+ /** True until the categories have loaded. */
92
+ IsLoading = true;
93
+ /** True while a category is being created or deleted. */
94
+ IsSaving = false;
95
+ /** The category tree, one row per category, depth first. */
96
+ Rows = [];
97
+ /** The name typed in the New category box. */
98
+ NewName = '';
99
+ /** The longest category name the box accepts. */
100
+ MaxNameLength = CATEGORY_NAME_MAX_LENGTH;
101
+ constructor(cdr) {
102
+ super();
103
+ this.cdr = cdr;
104
+ }
105
+ ngOnInit() {
106
+ super.ngOnInit();
107
+ // The dashboard cache also changes while this page is a background tab (categories created or
108
+ // deleted and dashboards moved in the Library or another session), so this only refreshes the view.
109
+ ObserveDashboardLibraryChanges()
110
+ .pipe(takeUntil(this.destroy$))
111
+ .subscribe(() => this.onCacheChanged());
112
+ this.navigationService.SetAgentClientTools(this, this.buildAgentTools());
113
+ void this.load();
114
+ }
115
+ ngOnDestroy() {
116
+ super.ngOnDestroy();
117
+ }
118
+ async GetResourceDisplayName(_data) {
119
+ return 'Categories';
120
+ }
121
+ async GetResourceIconClass(_data) {
122
+ return 'fa-solid fa-folder-tree';
123
+ }
124
+ /** True when the New category box holds a name and nothing is being saved. */
125
+ get CanCreate() {
126
+ return this.NewName.trim().length > 0 && !this.IsSaving;
127
+ }
128
+ /** The row's summary line, for example "3 dashboards · 1 sub-category". */
129
+ Summary(row) {
130
+ return CategoryRowSummary(row);
131
+ }
132
+ /** Creates a top-level category owned by the user from the New category box, then reloads the tree. */
133
+ async Create() {
134
+ if (!this.CanCreate) {
135
+ return;
136
+ }
137
+ const name = this.NewName.trim();
138
+ this.setSaving(true);
139
+ try {
140
+ const md = this.ProviderToUse;
141
+ const category = await md.GetEntityObject('MJ: Dashboard Categories', md.CurrentUser);
142
+ category.Name = name;
143
+ category.UserID = md.CurrentUser.ID;
144
+ if (!(await category.Save())) {
145
+ this.notifications.CreateSimpleNotification(category.LatestResult?.CompleteMessage || 'Could not create the category', 'error', 3000);
146
+ return;
147
+ }
148
+ this.NewName = '';
149
+ // The cache adds a saved category by itself only after an asynchronous copy; the reload makes
150
+ // the rows and the agent context include the new category now.
151
+ await this.reload();
152
+ this.notifications.CreateSimpleNotification(`Created "${name}"`, 'success', 2000);
153
+ }
154
+ catch (error) {
155
+ LogError(`Dashboards Categories: could not create "${name}": ${errorMessage(error)}`);
156
+ this.notifications.CreateSimpleNotification('Could not create the category', 'error', 3000);
157
+ }
158
+ finally {
159
+ this.setSaving(false);
160
+ }
161
+ }
162
+ /** Deletes an empty category the user owns, after the user confirms. */
163
+ async Delete(category) {
164
+ if (this.IsSaving) {
165
+ return;
166
+ }
167
+ const name = category.Name;
168
+ const confirmed = await this.confirmService.ConfirmDelete({
169
+ title: 'Delete category',
170
+ message: `Delete "${name}"?`,
171
+ detail: 'This action cannot be undone.',
172
+ });
173
+ if (!confirmed || this.IsSaving) {
174
+ return;
175
+ }
176
+ this.setSaving(true);
177
+ try {
178
+ if (await category.Delete()) {
179
+ // The cache removes a deleted category before Delete() resolves, so no reload is needed.
180
+ this.rebuildRows();
181
+ this.emitAgentContext();
182
+ this.notifications.CreateSimpleNotification(`Deleted "${name}"`, 'success', 2000);
183
+ }
184
+ else {
185
+ this.notifications.CreateSimpleNotification(category.LatestResult?.CompleteMessage || 'Could not delete the category', 'error', 3000);
186
+ }
187
+ }
188
+ catch (error) {
189
+ LogError(`Dashboards Categories: could not delete "${name}": ${errorMessage(error)}`);
190
+ this.notifications.CreateSimpleNotification('Could not delete the category', 'error', 3000);
191
+ }
192
+ finally {
193
+ this.setSaving(false);
194
+ }
195
+ }
196
+ /** Shows the category on the Library page of the Dashboards app. */
197
+ async OpenInLibrary(category) {
198
+ await this.openInLibrary(category);
199
+ }
200
+ async load() {
201
+ try {
202
+ await DashboardEngine.Instance.Config(false);
203
+ this.rebuildRows();
204
+ }
205
+ catch (error) {
206
+ LogError(`Dashboards Categories: could not load the categories: ${errorMessage(error)}`);
207
+ }
208
+ finally {
209
+ this.IsLoading = false;
210
+ this.emitAgentContext();
211
+ this.NotifyLoadComplete();
212
+ this.cdr.detectChanges();
213
+ }
214
+ }
215
+ /**
216
+ * Reloads the dashboard cache from the server, rebuilds the rows and reports them to the agent.
217
+ * Returns false when the reload fails.
218
+ */
219
+ async reload() {
220
+ try {
221
+ await DashboardEngine.Instance.Config(true);
222
+ this.rebuildRows();
223
+ this.emitAgentContext();
224
+ return true;
225
+ }
226
+ catch (error) {
227
+ LogError(`Dashboards Categories: could not reload the categories: ${errorMessage(error)}`);
228
+ return false;
229
+ }
230
+ finally {
231
+ this.cdr.detectChanges();
232
+ }
233
+ }
234
+ onCacheChanged() {
235
+ this.rebuildRows();
236
+ this.cdr.detectChanges();
237
+ }
238
+ rebuildRows() {
239
+ const engine = DashboardEngine.Instance;
240
+ const userId = this.ProviderToUse.CurrentUser.ID;
241
+ const dashboards = engine.GetAccessibleDashboards(userId);
242
+ const effectiveCategoryMap = BuildEffectiveCategoryMap(dashboards, engine.DashboardCategoryLinks, userId);
243
+ this.Rows = BuildCategoryRows(engine.GetAccessibleCategories(userId), dashboards, effectiveCategoryMap, userId);
244
+ }
245
+ async openInLibrary(category) {
246
+ const app = FindDashboardsApp(this.appManager);
247
+ if (!app) {
248
+ this.notifications.CreateSimpleNotification('The Dashboards app is not available', 'error', 3000);
249
+ return { Success: false, ErrorMessage: 'The Dashboards app is not available to this user.' };
250
+ }
251
+ try {
252
+ await this.navigationService.SwitchToApp(app.ID, DASHBOARDS_LIBRARY_NAV_ITEM, LocationQueryParams({ Filter: 'all', CategoryId: category.ID }));
253
+ return { Success: true };
254
+ }
255
+ catch (error) {
256
+ LogError(`Dashboards Categories: could not open "${category.Name}" in the Library: ${errorMessage(error)}`);
257
+ return { Success: false, ErrorMessage: errorMessage(error) };
258
+ }
259
+ }
260
+ setSaving(saving) {
261
+ this.IsSaving = saving;
262
+ this.cdr.detectChanges();
263
+ }
264
+ // ========================================
265
+ // Agent Context & Client Tools
266
+ //
267
+ // 🔒 SAFETY BOUNDARY: the Categories page exposes ONLY read-only /
268
+ // navigational tools to the AI agent: OpenCategoryInLibrary (shows a category
269
+ // on the Library page) and RefreshCategories (reloads the tree).
270
+ //
271
+ // Mutating operations — create / delete / rename / move / share a category,
272
+ // and any change to a dashboard — are intentionally NOT exposed. The agent
273
+ // helps the user find and understand categories; the user performs every
274
+ // mutation from the UI (New category, Delete). Do NOT add a mutating tool
275
+ // here without revisiting this boundary.
276
+ // ========================================
277
+ emitAgentContext() {
278
+ this.navigationService.SetAgentContext(this, {
279
+ IsLoading: this.IsLoading,
280
+ CategoryCount: this.Rows.length,
281
+ Categories: this.Rows.slice(0, AGENT_CONTEXT_NAME_LIST_CAP).map(r => ({
282
+ Name: r.Category.Name,
283
+ Depth: r.Depth,
284
+ DashboardCount: r.DashboardCount,
285
+ SubCategoryCount: r.SubCategoryCount,
286
+ })),
287
+ });
288
+ }
289
+ buildAgentTools() {
290
+ return [
291
+ {
292
+ Name: 'OpenCategoryInLibrary',
293
+ Description: 'Show a category and its dashboards on the Library page. Accepts the category name or its ID.',
294
+ ParameterSchema: {
295
+ type: 'object',
296
+ properties: { category: { type: 'string', description: 'The category name or ID.' } },
297
+ required: ['category'],
298
+ },
299
+ Handler: async (params) => {
300
+ const lookup = ResolveByIdOrName(this.Rows.map(r => r.Category), params['category'], 'category');
301
+ return lookup.ok ? this.openInLibrary(lookup.value) : lookup.result;
302
+ },
303
+ },
304
+ {
305
+ Name: 'RefreshCategories',
306
+ Description: 'Reload the category tree from the server.',
307
+ ParameterSchema: { type: 'object', properties: {} },
308
+ Handler: async () => (await this.reload()) ? { Success: true } : { Success: false, ErrorMessage: 'The categories could not be reloaded.' },
309
+ },
310
+ ];
311
+ }
312
+ static ɵfac = function DashboardsCategoriesResourceComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || DashboardsCategoriesResourceComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
313
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: DashboardsCategoriesResourceComponent, selectors: [["mj-dashboards-categories-resource"]], standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 10, vars: 4, consts: [["Title", "Categories", "Icon", "fa-solid fa-folder-tree", "Subtitle", "Your category tree. Categories are per user and can be shared like dashboards."], ["actions", ""], ["placeholder", "New category name", "aria-label", "New category name", 1, "mj-input", "cat-new-name", 3, "ngModelChange", "keydown.enter", "maxlength", "ngModel"], ["mjButton", "", "variant", "primary", "size", "sm", 1, "cat-create", 3, "click", "disabled"], ["aria-hidden", "true", 1, "fa-solid", "fa-plus"], ["Text", "Loading categories...", "Size", "medium"], [1, "cat-row", 3, "padding-left"], ["Icon", "fa-solid fa-folder-open", "Title", "No categories yet", "Message", "Create one to organize your dashboards."], [1, "cat-row"], ["aria-hidden", "true", 1, "fa-solid", "fa-folder"], [1, "cat-main"], ["mjButton", "", "size", "sm", 1, "cat-open", 3, "click", "ariaLabel"], ["mjButton", "", "size", "sm", "variant", "danger", 1, "cat-delete", 3, "ariaLabel", "disabled"], ["mjButton", "", "size", "sm", "variant", "danger", 1, "cat-delete", 3, "click", "ariaLabel", "disabled"], ["aria-hidden", "true", 1, "fa-solid", "fa-trash"]], template: function DashboardsCategoriesResourceComponent_Template(rf, ctx) { if (rf & 1) {
314
+ i0.ɵɵelementStart(0, "mj-page-layout")(1, "mj-page-header", 0)(2, "div", 1)(3, "input", 2);
315
+ i0.ɵɵtwoWayListener("ngModelChange", function DashboardsCategoriesResourceComponent_Template_input_ngModelChange_3_listener($event) { i0.ɵɵtwoWayBindingSet(ctx.NewName, $event) || (ctx.NewName = $event); return $event; });
316
+ i0.ɵɵlistener("keydown.enter", function DashboardsCategoriesResourceComponent_Template_input_keydown_enter_3_listener() { return ctx.Create(); });
317
+ i0.ɵɵelementEnd();
318
+ i0.ɵɵelementStart(4, "button", 3);
319
+ i0.ɵɵlistener("click", function DashboardsCategoriesResourceComponent_Template_button_click_4_listener() { return ctx.Create(); });
320
+ i0.ɵɵelement(5, "i", 4);
321
+ i0.ɵɵtext(6, " New category ");
322
+ i0.ɵɵelementEnd()()();
323
+ i0.ɵɵelementStart(7, "mj-page-body");
324
+ i0.ɵɵconditionalCreate(8, DashboardsCategoriesResourceComponent_Conditional_8_Template, 1, 0, "mj-loading", 5)(9, DashboardsCategoriesResourceComponent_Conditional_9_Template, 3, 1);
325
+ i0.ɵɵelementEnd()();
326
+ } if (rf & 2) {
327
+ i0.ɵɵadvance(3);
328
+ i0.ɵɵproperty("maxlength", ctx.MaxNameLength);
329
+ i0.ɵɵtwoWayProperty("ngModel", ctx.NewName);
330
+ i0.ɵɵadvance();
331
+ i0.ɵɵproperty("disabled", !ctx.CanCreate);
332
+ i0.ɵɵadvance(4);
333
+ i0.ɵɵconditional(ctx.IsLoading ? 8 : 9);
334
+ } }, dependencies: [i1.DefaultValueAccessor, i1.NgControlStatus, i1.MaxLengthValidator, i1.NgModel, i2.MJButtonDirective, i2.MJPageHeaderComponent, i2.MJPageLayoutComponent, i2.MJPageBodyComponent, i2.MJEmptyStateComponent, i3.LoadingComponent], styles: ["[_nghost-%COMP%] {\n display: block;\n height: 100%;\n width: 100%;\n }\n .cat-new-name[_ngcontent-%COMP%] {\n width: 220px;\n max-width: 100%;\n }\n .cat-row[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--mj-border-subtle);\n }\n .cat-row[_ngcontent-%COMP%] > i[_ngcontent-%COMP%] {\n color: var(--mj-text-secondary);\n }\n .cat-main[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n }\n .cat-main[_ngcontent-%COMP%] b[_ngcontent-%COMP%] {\n font-size: 13px;\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .cat-main[_ngcontent-%COMP%] small[_ngcontent-%COMP%] {\n font-size: 12px;\n color: var(--mj-text-muted);\n }"], changeDetection: 0 });
335
+ };
336
+ DashboardsCategoriesResourceComponent = __decorate([
337
+ RegisterClass(BaseResourceComponent, 'DashboardsCategoriesResource')
338
+ ], DashboardsCategoriesResourceComponent);
339
+ export { DashboardsCategoriesResourceComponent };
340
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(DashboardsCategoriesResourceComponent, [{
341
+ type: Component,
342
+ args: [{ standalone: false, selector: 'mj-dashboards-categories-resource', template: `
343
+ <mj-page-layout>
344
+ <mj-page-header Title="Categories" Icon="fa-solid fa-folder-tree" Subtitle="Your category tree. Categories are per user and can be shared like dashboards.">
345
+ <div actions>
346
+ <input
347
+ class="mj-input cat-new-name"
348
+ placeholder="New category name"
349
+ aria-label="New category name"
350
+ [maxlength]="MaxNameLength"
351
+ [(ngModel)]="NewName"
352
+ (keydown.enter)="Create()" />
353
+ <button mjButton variant="primary" size="sm" class="cat-create" [disabled]="!CanCreate" (click)="Create()">
354
+ <i class="fa-solid fa-plus" aria-hidden="true"></i> New category
355
+ </button>
356
+ </div>
357
+ </mj-page-header>
358
+ <mj-page-body>
359
+ @if (IsLoading) {
360
+ <mj-loading Text="Loading categories..." Size="medium"></mj-loading>
361
+ } @else {
362
+ @for (row of Rows; track row.Category.ID) {
363
+ <div class="cat-row" [style.padding-left.px]="14 + row.Depth * 28">
364
+ <i class="fa-solid fa-folder" aria-hidden="true"></i>
365
+ <span class="cat-main">
366
+ <b>{{ row.Category.Name }}</b>
367
+ <small>{{ Summary(row) }}</small>
368
+ </span>
369
+ <button mjButton size="sm" class="cat-open" [ariaLabel]="'Open ' + row.Category.Name + ' in the Library'" (click)="OpenInLibrary(row.Category)">Open</button>
370
+ @if (row.CanDelete) {
371
+ <button mjButton size="sm" variant="danger" class="cat-delete" [ariaLabel]="'Delete ' + row.Category.Name" [disabled]="IsSaving" (click)="Delete(row.Category)">
372
+ <i class="fa-solid fa-trash" aria-hidden="true"></i> Delete
373
+ </button>
374
+ }
375
+ </div>
376
+ } @empty {
377
+ <mj-empty-state Icon="fa-solid fa-folder-open" Title="No categories yet" Message="Create one to organize your dashboards." />
378
+ }
379
+ }
380
+ </mj-page-body>
381
+ </mj-page-layout>
382
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["\n :host {\n display: block;\n height: 100%;\n width: 100%;\n }\n .cat-new-name {\n width: 220px;\n max-width: 100%;\n }\n .cat-row {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--mj-border-subtle);\n }\n .cat-row > i {\n color: var(--mj-text-secondary);\n }\n .cat-main {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n }\n .cat-main b {\n font-size: 13px;\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .cat-main small {\n font-size: 12px;\n color: var(--mj-text-muted);\n }\n "] }]
383
+ }], () => [{ type: i0.ChangeDetectorRef }], null); })();
384
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(DashboardsCategoriesResourceComponent, { className: "DashboardsCategoriesResourceComponent", filePath: "src/DashboardBrowser/dashboards-categories-resource.component.ts", lineNumber: 116 }); })();
385
+ function errorMessage(error) {
386
+ return error instanceof Error ? error.message : String(error);
387
+ }
388
+ export function LoadDashboardsCategoriesResource() {
389
+ // Prevents tree-shaking of the component and its @RegisterClass registration.
390
+ }
391
+ //# sourceMappingURL=dashboards-categories-resource.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboards-categories-resource.component.js","sourceRoot":"","sources":["../../src/DashboardBrowser/dashboards-categories-resource.component.ts"],"names":[],"mappings":"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","sourcesContent":["import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core';\nimport { takeUntil } from 'rxjs/operators';\nimport { LogError } from '@memberjunction/core';\nimport { RegisterClass } from '@memberjunction/global';\nimport { DashboardEngine, MJDashboardCategoryEntity, ResourceData } from '@memberjunction/core-entities';\nimport { BaseResourceComponent } from '@memberjunction/ng-shared';\nimport { ApplicationManager } from '@memberjunction/ng-base-application';\nimport { MJNotificationService } from '@memberjunction/ng-notifications';\nimport { MJConfirmService } from '@memberjunction/ng-ui-components';\nimport { AGENT_CONTEXT_NAME_LIST_CAP, type AgentToolResult } from '../shared/agent-tool-validation';\nimport { DASHBOARDS_LIBRARY_NAV_ITEM, FindDashboardsApp } from '../shared/dashboards-app.helpers';\nimport { ObserveDashboardLibraryChanges } from '../shared/dashboard-library-changes';\nimport { LocationQueryParams } from './dashboard-library-filter';\nimport { DashboardsAgentClientTool, ResolveByIdOrName } from './dashboards-agent-tools';\nimport {\n BuildCategoryRows,\n BuildEffectiveCategoryMap,\n CATEGORY_NAME_MAX_LENGTH,\n CategoryRow,\n CategoryRowSummary,\n} from './dashboards-categories.helpers';\n\n/**\n * Categories page of the Dashboards app: the user's category tree with dashboard and sub-category\n * counts, a New category box, Delete, after a confirm, for an empty category the user owns, and Open,\n * which shows the category on the Library page. The agent tools are read-only; see the SAFETY\n * BOUNDARY in the agent section below.\n */\n@RegisterClass(BaseResourceComponent, 'DashboardsCategoriesResource')\n@Component({\n standalone: false,\n selector: 'mj-dashboards-categories-resource',\n template: `\n <mj-page-layout>\n <mj-page-header Title=\"Categories\" Icon=\"fa-solid fa-folder-tree\" Subtitle=\"Your category tree. Categories are per user and can be shared like dashboards.\">\n <div actions>\n <input\n class=\"mj-input cat-new-name\"\n placeholder=\"New category name\"\n aria-label=\"New category name\"\n [maxlength]=\"MaxNameLength\"\n [(ngModel)]=\"NewName\"\n (keydown.enter)=\"Create()\" />\n <button mjButton variant=\"primary\" size=\"sm\" class=\"cat-create\" [disabled]=\"!CanCreate\" (click)=\"Create()\">\n <i class=\"fa-solid fa-plus\" aria-hidden=\"true\"></i> New category\n </button>\n </div>\n </mj-page-header>\n <mj-page-body>\n @if (IsLoading) {\n <mj-loading Text=\"Loading categories...\" Size=\"medium\"></mj-loading>\n } @else {\n @for (row of Rows; track row.Category.ID) {\n <div class=\"cat-row\" [style.padding-left.px]=\"14 + row.Depth * 28\">\n <i class=\"fa-solid fa-folder\" aria-hidden=\"true\"></i>\n <span class=\"cat-main\">\n <b>{{ row.Category.Name }}</b>\n <small>{{ Summary(row) }}</small>\n </span>\n <button mjButton size=\"sm\" class=\"cat-open\" [ariaLabel]=\"'Open ' + row.Category.Name + ' in the Library'\" (click)=\"OpenInLibrary(row.Category)\">Open</button>\n @if (row.CanDelete) {\n <button mjButton size=\"sm\" variant=\"danger\" class=\"cat-delete\" [ariaLabel]=\"'Delete ' + row.Category.Name\" [disabled]=\"IsSaving\" (click)=\"Delete(row.Category)\">\n <i class=\"fa-solid fa-trash\" aria-hidden=\"true\"></i> Delete\n </button>\n }\n </div>\n } @empty {\n <mj-empty-state Icon=\"fa-solid fa-folder-open\" Title=\"No categories yet\" Message=\"Create one to organize your dashboards.\" />\n }\n }\n </mj-page-body>\n </mj-page-layout>\n `,\n styles: [\n `\n :host {\n display: block;\n height: 100%;\n width: 100%;\n }\n .cat-new-name {\n width: 220px;\n max-width: 100%;\n }\n .cat-row {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--mj-border-subtle);\n }\n .cat-row > i {\n color: var(--mj-text-secondary);\n }\n .cat-main {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n }\n .cat-main b {\n font-size: 13px;\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .cat-main small {\n font-size: 12px;\n color: var(--mj-text-muted);\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class DashboardsCategoriesResourceComponent extends BaseResourceComponent implements OnInit, OnDestroy {\n private appManager = inject(ApplicationManager);\n private notifications = inject(MJNotificationService);\n private confirmService = inject(MJConfirmService);\n\n /** True until the categories have loaded. */\n public IsLoading = true;\n\n /** True while a category is being created or deleted. */\n public IsSaving = false;\n\n /** The category tree, one row per category, depth first. */\n public Rows: CategoryRow[] = [];\n\n /** The name typed in the New category box. */\n public NewName = '';\n\n /** The longest category name the box accepts. */\n public readonly MaxNameLength = CATEGORY_NAME_MAX_LENGTH;\n\n constructor(private cdr: ChangeDetectorRef) {\n super();\n }\n\n ngOnInit(): void {\n super.ngOnInit();\n // The dashboard cache also changes while this page is a background tab (categories created or\n // deleted and dashboards moved in the Library or another session), so this only refreshes the view.\n ObserveDashboardLibraryChanges()\n .pipe(takeUntil(this.destroy$))\n .subscribe(() => this.onCacheChanged());\n this.navigationService.SetAgentClientTools(this, this.buildAgentTools());\n void this.load();\n }\n\n ngOnDestroy(): void {\n super.ngOnDestroy();\n }\n\n async GetResourceDisplayName(_data: ResourceData): Promise<string> {\n return 'Categories';\n }\n\n async GetResourceIconClass(_data: ResourceData): Promise<string> {\n return 'fa-solid fa-folder-tree';\n }\n\n /** True when the New category box holds a name and nothing is being saved. */\n public get CanCreate(): boolean {\n return this.NewName.trim().length > 0 && !this.IsSaving;\n }\n\n /** The row's summary line, for example \"3 dashboards · 1 sub-category\". */\n public Summary(row: CategoryRow): string {\n return CategoryRowSummary(row);\n }\n\n /** Creates a top-level category owned by the user from the New category box, then reloads the tree. */\n public async Create(): Promise<void> {\n if (!this.CanCreate) {\n return;\n }\n const name = this.NewName.trim();\n this.setSaving(true);\n try {\n const md = this.ProviderToUse;\n const category = await md.GetEntityObject<MJDashboardCategoryEntity>('MJ: Dashboard Categories', md.CurrentUser);\n category.Name = name;\n category.UserID = md.CurrentUser.ID;\n if (!(await category.Save())) {\n this.notifications.CreateSimpleNotification(category.LatestResult?.CompleteMessage || 'Could not create the category', 'error', 3000);\n return;\n }\n this.NewName = '';\n // The cache adds a saved category by itself only after an asynchronous copy; the reload makes\n // the rows and the agent context include the new category now.\n await this.reload();\n this.notifications.CreateSimpleNotification(`Created \"${name}\"`, 'success', 2000);\n } catch (error) {\n LogError(`Dashboards Categories: could not create \"${name}\": ${errorMessage(error)}`);\n this.notifications.CreateSimpleNotification('Could not create the category', 'error', 3000);\n } finally {\n this.setSaving(false);\n }\n }\n\n /** Deletes an empty category the user owns, after the user confirms. */\n public async Delete(category: MJDashboardCategoryEntity): Promise<void> {\n if (this.IsSaving) {\n return;\n }\n const name = category.Name;\n const confirmed = await this.confirmService.ConfirmDelete({\n title: 'Delete category',\n message: `Delete \"${name}\"?`,\n detail: 'This action cannot be undone.',\n });\n if (!confirmed || this.IsSaving) {\n return;\n }\n this.setSaving(true);\n try {\n if (await category.Delete()) {\n // The cache removes a deleted category before Delete() resolves, so no reload is needed.\n this.rebuildRows();\n this.emitAgentContext();\n this.notifications.CreateSimpleNotification(`Deleted \"${name}\"`, 'success', 2000);\n } else {\n this.notifications.CreateSimpleNotification(category.LatestResult?.CompleteMessage || 'Could not delete the category', 'error', 3000);\n }\n } catch (error) {\n LogError(`Dashboards Categories: could not delete \"${name}\": ${errorMessage(error)}`);\n this.notifications.CreateSimpleNotification('Could not delete the category', 'error', 3000);\n } finally {\n this.setSaving(false);\n }\n }\n\n /** Shows the category on the Library page of the Dashboards app. */\n public async OpenInLibrary(category: MJDashboardCategoryEntity): Promise<void> {\n await this.openInLibrary(category);\n }\n\n private async load(): Promise<void> {\n try {\n await DashboardEngine.Instance.Config(false);\n this.rebuildRows();\n } catch (error) {\n LogError(`Dashboards Categories: could not load the categories: ${errorMessage(error)}`);\n } finally {\n this.IsLoading = false;\n this.emitAgentContext();\n this.NotifyLoadComplete();\n this.cdr.detectChanges();\n }\n }\n\n /**\n * Reloads the dashboard cache from the server, rebuilds the rows and reports them to the agent.\n * Returns false when the reload fails.\n */\n private async reload(): Promise<boolean> {\n try {\n await DashboardEngine.Instance.Config(true);\n this.rebuildRows();\n this.emitAgentContext();\n return true;\n } catch (error) {\n LogError(`Dashboards Categories: could not reload the categories: ${errorMessage(error)}`);\n return false;\n } finally {\n this.cdr.detectChanges();\n }\n }\n\n private onCacheChanged(): void {\n this.rebuildRows();\n this.cdr.detectChanges();\n }\n\n private rebuildRows(): void {\n const engine = DashboardEngine.Instance;\n const userId = this.ProviderToUse.CurrentUser.ID;\n const dashboards = engine.GetAccessibleDashboards(userId);\n const effectiveCategoryMap = BuildEffectiveCategoryMap(dashboards, engine.DashboardCategoryLinks, userId);\n this.Rows = BuildCategoryRows(engine.GetAccessibleCategories(userId), dashboards, effectiveCategoryMap, userId);\n }\n\n private async openInLibrary(category: MJDashboardCategoryEntity): Promise<AgentToolResult> {\n const app = FindDashboardsApp(this.appManager);\n if (!app) {\n this.notifications.CreateSimpleNotification('The Dashboards app is not available', 'error', 3000);\n return { Success: false, ErrorMessage: 'The Dashboards app is not available to this user.' };\n }\n try {\n await this.navigationService.SwitchToApp(app.ID, DASHBOARDS_LIBRARY_NAV_ITEM, LocationQueryParams({ Filter: 'all', CategoryId: category.ID }));\n return { Success: true };\n } catch (error) {\n LogError(`Dashboards Categories: could not open \"${category.Name}\" in the Library: ${errorMessage(error)}`);\n return { Success: false, ErrorMessage: errorMessage(error) };\n }\n }\n\n private setSaving(saving: boolean): void {\n this.IsSaving = saving;\n this.cdr.detectChanges();\n }\n\n // ========================================\n // Agent Context & Client Tools\n //\n // 🔒 SAFETY BOUNDARY: the Categories page exposes ONLY read-only /\n // navigational tools to the AI agent: OpenCategoryInLibrary (shows a category\n // on the Library page) and RefreshCategories (reloads the tree).\n //\n // Mutating operations — create / delete / rename / move / share a category,\n // and any change to a dashboard — are intentionally NOT exposed. The agent\n // helps the user find and understand categories; the user performs every\n // mutation from the UI (New category, Delete). Do NOT add a mutating tool\n // here without revisiting this boundary.\n // ========================================\n\n private emitAgentContext(): void {\n this.navigationService.SetAgentContext(this, {\n IsLoading: this.IsLoading,\n CategoryCount: this.Rows.length,\n Categories: this.Rows.slice(0, AGENT_CONTEXT_NAME_LIST_CAP).map(r => ({\n Name: r.Category.Name,\n Depth: r.Depth,\n DashboardCount: r.DashboardCount,\n SubCategoryCount: r.SubCategoryCount,\n })),\n });\n }\n\n private buildAgentTools(): DashboardsAgentClientTool[] {\n return [\n {\n Name: 'OpenCategoryInLibrary',\n Description: 'Show a category and its dashboards on the Library page. Accepts the category name or its ID.',\n ParameterSchema: {\n type: 'object',\n properties: { category: { type: 'string', description: 'The category name or ID.' } },\n required: ['category'],\n },\n Handler: async (params: Record<string, unknown>): Promise<AgentToolResult> => {\n const lookup = ResolveByIdOrName(this.Rows.map(r => r.Category), params['category'], 'category');\n return lookup.ok ? this.openInLibrary(lookup.value) : lookup.result;\n },\n },\n {\n Name: 'RefreshCategories',\n Description: 'Reload the category tree from the server.',\n ParameterSchema: { type: 'object', properties: {} },\n Handler: async (): Promise<AgentToolResult> =>\n (await this.reload()) ? { Success: true } : { Success: false, ErrorMessage: 'The categories could not be reloaded.' },\n },\n ];\n }\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nexport function LoadDashboardsCategoriesResource(): void {\n // Prevents tree-shaking of the component and its @RegisterClass registration.\n}\n"]}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @fileoverview Pure logic for the Dashboards app's Categories page: the category tree as rows,
3
+ * the dashboard count of each category, and which categories can be deleted.
4
+ *
5
+ * A row's dashboard count follows the rules of the Library page, so it equals the number of
6
+ * dashboards the Library shows when the category is opened there.
7
+ */
8
+ import type { MJDashboardCategoryEntity, MJDashboardCategoryLinkEntity, MJDashboardEntity } from '@memberjunction/core-entities';
9
+ /** The longest name the MJ: Dashboard Categories entity stores. */
10
+ export declare const CATEGORY_NAME_MAX_LENGTH = 100;
11
+ /** One row of the Categories page. */
12
+ export interface CategoryRow {
13
+ Category: MJDashboardCategoryEntity;
14
+ /** 0 for a top-level category, 1 for its sub-categories, and so on. */
15
+ Depth: number;
16
+ /** The Config dashboards the Library shows in this category. */
17
+ DashboardCount: number;
18
+ /** The direct sub-categories. */
19
+ SubCategoryCount: number;
20
+ /** True when the user owns the category and it holds no sub-categories and no dashboards of any type. */
21
+ CanDelete: boolean;
22
+ }
23
+ /**
24
+ * The category each shared dashboard shows under for the user: the category the user filed it in
25
+ * (an MJ: Dashboard Category Links row of the user), or null for the root. Dashboards the user owns
26
+ * are not in the map; they show under their own category.
27
+ */
28
+ export declare function BuildEffectiveCategoryMap(dashboards: MJDashboardEntity[], links: MJDashboardCategoryLinkEntity[], currentUserId: string): Map<string, string | null>;
29
+ /**
30
+ * The category tree as rows, depth first, each level sorted by name. A category whose parent is not
31
+ * in `categories` is shown at the top level. Categories that only reach each other through their
32
+ * parents are not shown.
33
+ */
34
+ export declare function BuildCategoryRows(categories: MJDashboardCategoryEntity[], dashboards: MJDashboardEntity[], effectiveCategoryMap: Map<string, string | null>, currentUserId: string): CategoryRow[];
35
+ /** The row's summary line, for example "3 dashboards · 1 sub-category". */
36
+ export declare function CategoryRowSummary(row: CategoryRow): string;
37
+ //# sourceMappingURL=dashboards-categories.helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboards-categories.helpers.d.ts","sourceRoot":"","sources":["../../src/DashboardBrowser/dashboards-categories.helpers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,yBAAyB,EAAE,6BAA6B,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAIjI,mEAAmE;AACnE,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,sCAAsC;AACtC,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,yBAAyB,CAAC;IACpC,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,cAAc,EAAE,MAAM,CAAC;IACvB,iCAAiC;IACjC,gBAAgB,EAAE,MAAM,CAAC;IACzB,yGAAyG;IACzG,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,iBAAiB,EAAE,EAC/B,KAAK,EAAE,6BAA6B,EAAE,EACtC,aAAa,EAAE,MAAM,GACpB,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAW5B;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,yBAAyB,EAAE,EACvC,UAAU,EAAE,iBAAiB,EAAE,EAC/B,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,EAChD,aAAa,EAAE,MAAM,GACpB,WAAW,EAAE,CAqBf;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAM3D"}
@@ -0,0 +1,68 @@
1
+ import { IndexByUUID, UUIDsEqual } from '@memberjunction/global';
2
+ import { CountDashboardsByCategory, EffectiveCategoryId } from './dashboard-library-filter';
3
+ /** The longest name the MJ: Dashboard Categories entity stores. */
4
+ export const CATEGORY_NAME_MAX_LENGTH = 100;
5
+ /**
6
+ * The category each shared dashboard shows under for the user: the category the user filed it in
7
+ * (an MJ: Dashboard Category Links row of the user), or null for the root. Dashboards the user owns
8
+ * are not in the map; they show under their own category.
9
+ */
10
+ export function BuildEffectiveCategoryMap(dashboards, links, currentUserId) {
11
+ const userLinksByDashboard = IndexByUUID(links.filter(l => UUIDsEqual(l.UserID, currentUserId)), l => l.DashboardID);
12
+ const map = new Map();
13
+ for (const dashboard of dashboards) {
14
+ if (UUIDsEqual(dashboard.UserID, currentUserId)) {
15
+ continue;
16
+ }
17
+ const filed = userLinksByDashboard.Get(dashboard.ID);
18
+ map.set(dashboard.ID, filed ? filed.DashboardCategoryID : null);
19
+ }
20
+ return map;
21
+ }
22
+ /**
23
+ * The category tree as rows, depth first, each level sorted by name. A category whose parent is not
24
+ * in `categories` is shown at the top level. Categories that only reach each other through their
25
+ * parents are not shown.
26
+ */
27
+ export function BuildCategoryRows(categories, dashboards, effectiveCategoryMap, currentUserId) {
28
+ const counts = CountDashboardsByCategory(dashboards, categories, effectiveCategoryMap).ByCategory;
29
+ const rows = [];
30
+ const visited = new Set();
31
+ const visit = (category, depth) => {
32
+ if (visited.has(category)) {
33
+ return;
34
+ }
35
+ visited.add(category);
36
+ const children = sortByName(categories.filter(c => c.ParentID != null && UUIDsEqual(c.ParentID, category.ID)));
37
+ rows.push({
38
+ Category: category,
39
+ Depth: depth,
40
+ DashboardCount: counts.get(category.ID) ?? 0,
41
+ SubCategoryCount: children.length,
42
+ CanDelete: UUIDsEqual(category.UserID, currentUserId) && children.length === 0 && !holdsDashboards(category, dashboards, effectiveCategoryMap),
43
+ });
44
+ children.forEach(child => visit(child, depth + 1));
45
+ };
46
+ sortByName(categories.filter(c => isTopLevel(c, categories))).forEach(c => visit(c, 0));
47
+ return rows;
48
+ }
49
+ /** The row's summary line, for example "3 dashboards · 1 sub-category". */
50
+ export function CategoryRowSummary(row) {
51
+ const dashboards = `${row.DashboardCount} ${row.DashboardCount === 1 ? 'dashboard' : 'dashboards'}`;
52
+ if (!row.SubCategoryCount) {
53
+ return dashboards;
54
+ }
55
+ return `${dashboards} · ${row.SubCategoryCount} ${row.SubCategoryCount === 1 ? 'sub-category' : 'sub-categories'}`;
56
+ }
57
+ /** True when a dashboard of any type is in the category, or is filed there by the user. */
58
+ function holdsDashboards(category, dashboards, effectiveCategoryMap) {
59
+ return dashboards.some(d => UUIDsEqual(d.CategoryID, category.ID) || UUIDsEqual(EffectiveCategoryId(d, effectiveCategoryMap), category.ID));
60
+ }
61
+ /** True when a category has no parent, or its parent is not in `categories`. */
62
+ function isTopLevel(category, categories) {
63
+ return category.ParentID == null || !categories.some(c => UUIDsEqual(c.ID, category.ParentID));
64
+ }
65
+ function sortByName(categories) {
66
+ return [...categories].sort((a, b) => a.Name.localeCompare(b.Name));
67
+ }
68
+ //# sourceMappingURL=dashboards-categories.helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboards-categories.helpers.js","sourceRoot":"","sources":["../../src/DashboardBrowser/dashboards-categories.helpers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Pure logic for the Dashboards app's Categories page: the category tree as rows,\n * the dashboard count of each category, and which categories can be deleted.\n *\n * A row's dashboard count follows the rules of the Library page, so it equals the number of\n * dashboards the Library shows when the category is opened there.\n */\nimport type { MJDashboardCategoryEntity, MJDashboardCategoryLinkEntity, MJDashboardEntity } from '@memberjunction/core-entities';\nimport { IndexByUUID, UUIDsEqual } from '@memberjunction/global';\nimport { CountDashboardsByCategory, EffectiveCategoryId } from './dashboard-library-filter';\n\n/** The longest name the MJ: Dashboard Categories entity stores. */\nexport const CATEGORY_NAME_MAX_LENGTH = 100;\n\n/** One row of the Categories page. */\nexport interface CategoryRow {\n Category: MJDashboardCategoryEntity;\n /** 0 for a top-level category, 1 for its sub-categories, and so on. */\n Depth: number;\n /** The Config dashboards the Library shows in this category. */\n DashboardCount: number;\n /** The direct sub-categories. */\n SubCategoryCount: number;\n /** True when the user owns the category and it holds no sub-categories and no dashboards of any type. */\n CanDelete: boolean;\n}\n\n/**\n * The category each shared dashboard shows under for the user: the category the user filed it in\n * (an MJ: Dashboard Category Links row of the user), or null for the root. Dashboards the user owns\n * are not in the map; they show under their own category.\n */\nexport function BuildEffectiveCategoryMap(\n dashboards: MJDashboardEntity[],\n links: MJDashboardCategoryLinkEntity[],\n currentUserId: string\n): Map<string, string | null> {\n const userLinksByDashboard = IndexByUUID(links.filter(l => UUIDsEqual(l.UserID, currentUserId)), l => l.DashboardID);\n const map = new Map<string, string | null>();\n for (const dashboard of dashboards) {\n if (UUIDsEqual(dashboard.UserID, currentUserId)) {\n continue;\n }\n const filed = userLinksByDashboard.Get(dashboard.ID);\n map.set(dashboard.ID, filed ? filed.DashboardCategoryID : null);\n }\n return map;\n}\n\n/**\n * The category tree as rows, depth first, each level sorted by name. A category whose parent is not\n * in `categories` is shown at the top level. Categories that only reach each other through their\n * parents are not shown.\n */\nexport function BuildCategoryRows(\n categories: MJDashboardCategoryEntity[],\n dashboards: MJDashboardEntity[],\n effectiveCategoryMap: Map<string, string | null>,\n currentUserId: string\n): CategoryRow[] {\n const counts = CountDashboardsByCategory(dashboards, categories, effectiveCategoryMap).ByCategory;\n const rows: CategoryRow[] = [];\n const visited = new Set<MJDashboardCategoryEntity>();\n const visit = (category: MJDashboardCategoryEntity, depth: number): void => {\n if (visited.has(category)) {\n return;\n }\n visited.add(category);\n const children = sortByName(categories.filter(c => c.ParentID != null && UUIDsEqual(c.ParentID, category.ID)));\n rows.push({\n Category: category,\n Depth: depth,\n DashboardCount: counts.get(category.ID) ?? 0,\n SubCategoryCount: children.length,\n CanDelete: UUIDsEqual(category.UserID, currentUserId) && children.length === 0 && !holdsDashboards(category, dashboards, effectiveCategoryMap),\n });\n children.forEach(child => visit(child, depth + 1));\n };\n sortByName(categories.filter(c => isTopLevel(c, categories))).forEach(c => visit(c, 0));\n return rows;\n}\n\n/** The row's summary line, for example \"3 dashboards · 1 sub-category\". */\nexport function CategoryRowSummary(row: CategoryRow): string {\n const dashboards = `${row.DashboardCount} ${row.DashboardCount === 1 ? 'dashboard' : 'dashboards'}`;\n if (!row.SubCategoryCount) {\n return dashboards;\n }\n return `${dashboards} · ${row.SubCategoryCount} ${row.SubCategoryCount === 1 ? 'sub-category' : 'sub-categories'}`;\n}\n\n/** True when a dashboard of any type is in the category, or is filed there by the user. */\nfunction holdsDashboards(\n category: MJDashboardCategoryEntity,\n dashboards: MJDashboardEntity[],\n effectiveCategoryMap: Map<string, string | null>\n): boolean {\n return dashboards.some(d => UUIDsEqual(d.CategoryID, category.ID) || UUIDsEqual(EffectiveCategoryId(d, effectiveCategoryMap), category.ID));\n}\n\n/** True when a category has no parent, or its parent is not in `categories`. */\nfunction isTopLevel(category: MJDashboardCategoryEntity, categories: MJDashboardCategoryEntity[]): boolean {\n return category.ParentID == null || !categories.some(c => UUIDsEqual(c.ID, category.ParentID));\n}\n\nfunction sortByName(categories: MJDashboardCategoryEntity[]): MJDashboardCategoryEntity[] {\n return [...categories].sort((a, b) => a.Name.localeCompare(b.Name));\n}\n"]}