@carlonicora/nextjs-jsonapi 5.4.0 → 5.6.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 (237) hide show
  1. package/dist/{BlockNoteEditor-4YCAIEPT.js → BlockNoteEditor-AE3ZTEEH.js} +20 -20
  2. package/dist/{BlockNoteEditor-4YCAIEPT.js.map → BlockNoteEditor-AE3ZTEEH.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-YQL2SGFR.mjs → BlockNoteEditor-NEYG5L6W.mjs} +5 -5
  4. package/dist/{BlockNoteViewer-AEJKJ27Q.mjs → BlockNoteViewer-3OUT5UFY.mjs} +3 -3
  5. package/dist/{BlockNoteViewer-YL3J7J2Y.js → BlockNoteViewer-4CFKQM7U.js} +4 -4
  6. package/dist/{BlockNoteViewer-YL3J7J2Y.js.map → BlockNoteViewer-4CFKQM7U.js.map} +1 -1
  7. package/dist/billing/index.d.mts +186 -25
  8. package/dist/billing/index.d.ts +186 -25
  9. package/dist/billing/index.js +1149 -588
  10. package/dist/billing/index.js.map +1 -1
  11. package/dist/billing/index.mjs +786 -225
  12. package/dist/billing/index.mjs.map +1 -1
  13. package/dist/{chunk-5PPG6PL7.js → chunk-3I5AAJXV.js} +3 -3
  14. package/dist/{chunk-5PPG6PL7.js.map → chunk-3I5AAJXV.js.map} +1 -1
  15. package/dist/{chunk-YW4ZCEWB.js → chunk-53BAULDP.js} +4 -4
  16. package/dist/{chunk-YW4ZCEWB.js.map → chunk-53BAULDP.js.map} +1 -1
  17. package/dist/{chunk-XCHSX3VC.mjs → chunk-BTPUYUQ3.mjs} +2 -2
  18. package/dist/{chunk-WC63PJSC.mjs → chunk-EYA2F6AN.mjs} +756 -24
  19. package/dist/chunk-EYA2F6AN.mjs.map +1 -0
  20. package/dist/{chunk-NFBOSPNJ.js → chunk-HKZAESS6.js} +954 -222
  21. package/dist/chunk-HKZAESS6.js.map +1 -0
  22. package/dist/{chunk-6DC2WVVB.mjs → chunk-KQHBONL4.mjs} +2 -2
  23. package/dist/{chunk-3NLAXVIN.js → chunk-LQ4DK4JF.js} +1988 -1089
  24. package/dist/chunk-LQ4DK4JF.js.map +1 -0
  25. package/dist/{chunk-YNCQSWQW.js → chunk-RL3PL3KV.js} +8 -8
  26. package/dist/{chunk-YNCQSWQW.js.map → chunk-RL3PL3KV.js.map} +1 -1
  27. package/dist/{chunk-EUKTBOI2.mjs → chunk-SFYPXCK4.mjs} +3382 -2483
  28. package/dist/chunk-SFYPXCK4.mjs.map +1 -0
  29. package/dist/{chunk-QMYQRPQC.mjs → chunk-Y7RR57EW.mjs} +2 -2
  30. package/dist/client/index.js +5 -5
  31. package/dist/client/index.js.map +1 -1
  32. package/dist/client/index.mjs +4 -4
  33. package/dist/components/editors/server.d.mts +15 -0
  34. package/dist/components/editors/server.d.ts +15 -0
  35. package/dist/components/editors/server.js +38 -0
  36. package/dist/components/editors/server.js.map +1 -0
  37. package/dist/components/editors/server.mjs +38 -0
  38. package/dist/components/editors/server.mjs.map +1 -0
  39. package/dist/components/index.d.mts +33 -10
  40. package/dist/components/index.d.ts +33 -10
  41. package/dist/components/index.js +7 -5
  42. package/dist/components/index.js.map +1 -1
  43. package/dist/components/index.mjs +8 -6
  44. package/dist/contexts/index.d.mts +9 -1
  45. package/dist/contexts/index.d.ts +9 -1
  46. package/dist/contexts/index.js +5 -5
  47. package/dist/contexts/index.mjs +4 -4
  48. package/dist/core/index.d.mts +242 -3
  49. package/dist/core/index.d.ts +242 -3
  50. package/dist/core/index.js +36 -4
  51. package/dist/core/index.js.map +1 -1
  52. package/dist/core/index.mjs +35 -3
  53. package/dist/email-automations/index.d.mts +3 -0
  54. package/dist/email-automations/index.d.ts +3 -0
  55. package/dist/email-automations/index.js +99 -95
  56. package/dist/email-automations/index.js.map +1 -1
  57. package/dist/email-automations/index.mjs +30 -26
  58. package/dist/email-automations/index.mjs.map +1 -1
  59. package/dist/features/analytics/index.d.mts +11 -4
  60. package/dist/features/analytics/index.d.ts +11 -4
  61. package/dist/features/analytics/index.js +127 -135
  62. package/dist/features/analytics/index.js.map +1 -1
  63. package/dist/features/analytics/index.mjs +16 -24
  64. package/dist/features/analytics/index.mjs.map +1 -1
  65. package/dist/features/analytics/server.d.mts +5 -2
  66. package/dist/features/analytics/server.d.ts +5 -2
  67. package/dist/features/analytics/server.js +7 -7
  68. package/dist/features/analytics/server.js.map +1 -1
  69. package/dist/features/analytics/server.mjs +5 -5
  70. package/dist/features/analytics/server.mjs.map +1 -1
  71. package/dist/features/blog/index.d.mts +12 -2
  72. package/dist/features/blog/index.d.ts +12 -2
  73. package/dist/features/blog/index.js +206 -195
  74. package/dist/features/blog/index.js.map +1 -1
  75. package/dist/features/blog/index.mjs +71 -60
  76. package/dist/features/blog/index.mjs.map +1 -1
  77. package/dist/features/blog/server.d.mts +4 -1
  78. package/dist/features/blog/server.d.ts +4 -1
  79. package/dist/features/blog/server.js +49 -21
  80. package/dist/features/blog/server.js.map +1 -1
  81. package/dist/features/blog/server.mjs +36 -8
  82. package/dist/features/blog/server.mjs.map +1 -1
  83. package/dist/features/help/index.js +38 -38
  84. package/dist/features/help/index.mjs +4 -4
  85. package/dist/features/tokenusage/index.d.mts +1 -3
  86. package/dist/features/tokenusage/index.d.ts +1 -3
  87. package/dist/features/tokenusage/index.js +127 -135
  88. package/dist/features/tokenusage/index.js.map +1 -1
  89. package/dist/features/tokenusage/index.mjs +10 -18
  90. package/dist/features/tokenusage/index.mjs.map +1 -1
  91. package/dist/{stripe-promotion-code.interface-C3qqh3mi.d.ts → format-options-BtX3o0Yu.d.ts} +104 -1
  92. package/dist/{stripe-promotion-code.interface-ClZ7DxS9.d.mts → format-options-D6c7FNDi.d.mts} +104 -1
  93. package/dist/index.d.mts +2 -2
  94. package/dist/index.d.ts +2 -2
  95. package/dist/index.js +37 -5
  96. package/dist/index.js.map +1 -1
  97. package/dist/index.mjs +36 -4
  98. package/dist/server/index.js +4 -4
  99. package/dist/server/index.mjs +3 -3
  100. package/messages/en.json +125 -3
  101. package/messages/it.json +125 -3
  102. package/package.json +9 -1
  103. package/scripts/validate-messages.mjs +1 -0
  104. package/src/billing/index.ts +1 -0
  105. package/src/client/index.ts +4 -0
  106. package/src/components/editors/server/BlockNoteServerContent.tsx +28 -0
  107. package/src/components/editors/server/__tests__/renderBlockNoteHTML.spec.ts +44 -0
  108. package/src/components/editors/server/renderBlockNoteHTML.ts +26 -0
  109. package/src/components/editors/server-entry.ts +7 -0
  110. package/src/components/index.ts +1 -0
  111. package/src/components/viewers/BlockNoteStaticViewer.tsx +44 -0
  112. package/src/core/index.ts +1 -0
  113. package/src/core/registry/ModuleRegistry.ts +3 -0
  114. package/src/features/administration/components/AdminIndexContainer.tsx +61 -33
  115. package/src/features/administration/components/__tests__/AdminIndexContainer.spec.tsx +61 -1
  116. package/src/features/administration/components/index.ts +2 -1
  117. package/src/features/administration/contexts/AdministrationContext.tsx +11 -3
  118. package/src/features/administration/contexts/__tests__/AdministrationContext.spec.tsx +40 -0
  119. package/src/features/ai-connection/ai-connection.module.ts +2 -0
  120. package/src/features/ai-connection/contexts/AiConnectionsContext.tsx +3 -16
  121. package/src/features/ai-connection/contexts/__tests__/AiConnectionsContext.spec.tsx +33 -0
  122. package/src/features/analytics/__tests__/server-entry.spec.tsx +2 -1
  123. package/src/features/analytics/analytics.modules.ts +22 -5
  124. package/src/features/analytics/components/AnalyticsAdminContainer.tsx +4 -3
  125. package/src/features/analytics/components/AnalyticsAdminPageContainer.tsx +6 -3
  126. package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +4 -3
  127. package/src/features/analytics/components/__tests__/AnalyticsJourneyContainer.spec.tsx +11 -0
  128. package/src/features/analytics/components/__tests__/AnalyticsPageContainers.spec.tsx +12 -1
  129. package/src/features/analytics/components/server/AnalyticsAdminPage.tsx +6 -3
  130. package/src/features/analytics/contexts/AnalyticsAdminContext.tsx +8 -17
  131. package/src/features/analytics/contexts/__tests__/AnalyticsAdminContext.spec.tsx +17 -0
  132. package/src/features/billing/components/cards/InvoicesSummaryCard.tsx +3 -2
  133. package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +4 -3
  134. package/src/features/billing/components/containers/BillingDashboardContainer.tsx +22 -5
  135. package/src/features/billing/components/utils/__tests__/discount.spec.ts +59 -0
  136. package/src/features/billing/components/utils/__tests__/url.spec.ts +32 -0
  137. package/src/features/billing/components/utils/discount.ts +38 -0
  138. package/src/features/billing/components/utils/index.ts +2 -0
  139. package/src/features/billing/components/utils/url.ts +12 -0
  140. package/src/features/billing/contexts/CouponContext.tsx +211 -0
  141. package/src/features/billing/contexts/index.ts +1 -0
  142. package/src/features/billing/i18n-keys.ts +79 -0
  143. package/src/features/billing/stripe-coupon/components/containers/CouponContainer.tsx +38 -0
  144. package/src/features/billing/stripe-coupon/components/containers/index.ts +1 -0
  145. package/src/features/billing/stripe-coupon/components/details/CouponDetails.tsx +69 -0
  146. package/src/features/billing/stripe-coupon/components/details/index.ts +1 -0
  147. package/src/features/billing/stripe-coupon/components/forms/CouponCodeEditor.tsx +79 -0
  148. package/src/features/billing/stripe-coupon/components/forms/CouponCodeFields.tsx +192 -0
  149. package/src/features/billing/stripe-coupon/components/forms/CouponEditor.tsx +333 -0
  150. package/src/features/billing/stripe-coupon/components/forms/CouponRefresher.tsx +45 -0
  151. package/src/features/billing/stripe-coupon/components/forms/index.ts +6 -0
  152. package/src/features/billing/stripe-coupon/components/index.ts +5 -0
  153. package/src/features/billing/stripe-coupon/components/lists/CouponCodesList.tsx +61 -0
  154. package/src/features/billing/stripe-coupon/components/lists/CouponsList.tsx +67 -0
  155. package/src/features/billing/stripe-coupon/components/lists/index.ts +2 -0
  156. package/src/features/billing/stripe-coupon/components/widgets/CouponCodeActions.tsx +80 -0
  157. package/src/features/billing/stripe-coupon/components/widgets/index.ts +1 -0
  158. package/src/features/billing/stripe-coupon/data/__tests__/stripe-coupon-models.spec.ts +114 -0
  159. package/src/features/billing/stripe-coupon/data/index.ts +10 -0
  160. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.fields.ts +8 -0
  161. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.interface.ts +44 -0
  162. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.service.ts +92 -0
  163. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.ts +132 -0
  164. package/src/features/billing/stripe-coupon/data/stripe-coupon-offer.interface.ts +17 -0
  165. package/src/features/billing/stripe-coupon/data/stripe-coupon-offer.ts +70 -0
  166. package/src/features/billing/stripe-coupon/data/stripe-coupon.fields.ts +7 -0
  167. package/src/features/billing/stripe-coupon/data/stripe-coupon.interface.ts +51 -0
  168. package/src/features/billing/stripe-coupon/data/stripe-coupon.service.ts +93 -0
  169. package/src/features/billing/stripe-coupon/data/stripe-coupon.ts +165 -0
  170. package/src/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.tsx +154 -0
  171. package/src/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.tsx +113 -0
  172. package/src/features/billing/stripe-coupon/index.ts +5 -0
  173. package/src/features/billing/stripe-coupon/stripe-coupon-code.module.ts +14 -0
  174. package/src/features/billing/stripe-coupon/stripe-coupon-offer.module.ts +10 -0
  175. package/src/features/billing/stripe-coupon/stripe-coupon.module.ts +18 -0
  176. package/src/features/billing/stripe-coupon/utils/__tests__/coupon-places.spec.ts +19 -0
  177. package/src/features/billing/stripe-coupon/utils/__tests__/to-promotion-code-result.spec.ts +41 -0
  178. package/src/features/billing/stripe-coupon/utils/coupon-places.ts +23 -0
  179. package/src/features/billing/stripe-coupon/utils/format-coupon-discount.ts +36 -0
  180. package/src/features/billing/stripe-coupon/utils/index.ts +3 -0
  181. package/src/features/billing/stripe-coupon/utils/to-promotion-code-result.ts +23 -0
  182. package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +24 -8
  183. package/src/features/billing/stripe-price/stripe-price.module.ts +2 -0
  184. package/src/features/billing/stripe-product/components/containers/ProductsListContainer.tsx +21 -6
  185. package/src/features/billing/stripe-product/stripe-product.module.ts +2 -0
  186. package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +60 -19
  187. package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.interface.ts +2 -0
  188. package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +24 -5
  189. package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +21 -2
  190. package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +1 -1
  191. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +1 -1
  192. package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +60 -49
  193. package/src/features/billing/stripe-subscription/components/wizards/__tests__/review-card.spec.ts +34 -0
  194. package/src/features/billing/stripe-subscription/components/wizards/review-card.ts +24 -0
  195. package/src/features/billing/stripe-subscription/data/__tests__/stripe-subscription.discount.spec.ts +67 -0
  196. package/src/features/billing/stripe-subscription/data/stripe-subscription.interface.ts +14 -0
  197. package/src/features/billing/stripe-subscription/data/stripe-subscription.service.ts +39 -0
  198. package/src/features/billing/stripe-subscription/data/stripe-subscription.ts +21 -0
  199. package/src/features/billing/stripe-subscription/hooks/__tests__/wizard-payment.spec.ts +48 -0
  200. package/src/features/billing/stripe-subscription/hooks/__tests__/wizard-promotion.spec.ts +132 -0
  201. package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +138 -12
  202. package/src/features/billing/stripe-subscription/hooks/wizard-payment.ts +30 -0
  203. package/src/features/billing/stripe-subscription/hooks/wizard-promotion.ts +54 -0
  204. package/src/features/blog/components/admin/BlogAdminContainer.tsx +13 -6
  205. package/src/features/blog/components/admin/BlogCategoriesList.tsx +1 -1
  206. package/src/features/blog/components/admin/BlogCoverImageField.tsx +1 -1
  207. package/src/features/blog/components/admin/BlogPostContainer.tsx +0 -1
  208. package/src/features/blog/components/admin/BlogPostEditor.tsx +42 -33
  209. package/src/features/blog/components/admin/BlogPostTranslationEditor.tsx +12 -4
  210. package/src/features/blog/components/admin/BlogPostsList.tsx +1 -1
  211. package/src/features/blog/components/public/BlogPostCard.tsx +2 -9
  212. package/src/features/blog/components/public/BlogPostMeta.tsx +2 -2
  213. package/src/features/blog/components/public/BlogPostView.tsx +9 -3
  214. package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +2 -0
  215. package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +17 -11
  216. package/src/features/blog/server/fetchers.ts +57 -8
  217. package/src/features/email-automation/components/containers/EmailAutomationsListContainer.tsx +12 -3
  218. package/src/features/rbac/contexts/RbacContext.tsx +4 -7
  219. package/src/features/rbac/contexts/__tests__/RbacContext.spec.tsx +42 -0
  220. package/src/features/tokenusage/components/TokenUsageAdminContainer.tsx +4 -3
  221. package/src/features/tokenusage/contexts/TokenUsageAdminContext.tsx +3 -18
  222. package/src/features/tokenusage/contexts/__tests__/TokenUsageAdminContext.spec.tsx +17 -0
  223. package/src/features/tokenusage/tokenusage-admin.module.ts +4 -3
  224. package/src/features/tokenusage/tokenusage.modules.ts +12 -2
  225. package/src/features/user/contexts/CurrentUserContext.tsx +25 -0
  226. package/src/features/user/contexts/UserContext.tsx +18 -3
  227. package/src/features/user/contexts/__tests__/CurrentUserContext.spec.tsx +121 -2
  228. package/src/features/user/contexts/__tests__/UserContext.spec.tsx +95 -0
  229. package/dist/chunk-3NLAXVIN.js.map +0 -1
  230. package/dist/chunk-EUKTBOI2.mjs.map +0 -1
  231. package/dist/chunk-NFBOSPNJ.js.map +0 -1
  232. package/dist/chunk-WC63PJSC.mjs.map +0 -1
  233. /package/dist/{BlockNoteEditor-YQL2SGFR.mjs.map → BlockNoteEditor-NEYG5L6W.mjs.map} +0 -0
  234. /package/dist/{BlockNoteViewer-AEJKJ27Q.mjs.map → BlockNoteViewer-3OUT5UFY.mjs.map} +0 -0
  235. /package/dist/{chunk-XCHSX3VC.mjs.map → chunk-BTPUYUQ3.mjs.map} +0 -0
  236. /package/dist/{chunk-6DC2WVVB.mjs.map → chunk-KQHBONL4.mjs.map} +0 -0
  237. /package/dist/{chunk-QMYQRPQC.mjs.map → chunk-Y7RR57EW.mjs.map} +0 -0
@@ -34,6 +34,9 @@ export interface FoundationModuleDefinitions {
34
34
  StripePrice: ModuleWithPermissions;
35
35
  StripeUsage: ModuleWithPermissions;
36
36
  StripePromotionCode: ModuleWithPermissions;
37
+ StripeCoupon: ModuleWithPermissions;
38
+ StripeCouponCode: ModuleWithPermissions;
39
+ StripeCouponOffer: ModuleWithPermissions;
37
40
  // AI connection administration
38
41
  AiConnection: ModuleWithPermissions;
39
42
  // OAuth modules
@@ -8,14 +8,21 @@ import { Link } from "../../../shadcnui";
8
8
  import { AdministrationProvider } from "../contexts/AdministrationContext";
9
9
  import type { AdminGroup, AdminSection } from "../data/admin-section.types";
10
10
 
11
+ /** Keys of the built-in platform sections, for `hiddenSections`. */
12
+ export type PlatformSectionKey = "companies" | "users" | "token-usage" | "ai-connections" | "rbac" | "billing";
13
+
11
14
  /**
12
- * The sections every NJA project has. Hrefs are constants rather than props:
13
- * the path convention IS the contract that makes this component reusable, and
14
- * it already exists in the package (see TOKEN_USAGE_ADMIN_PAGE_URL in
15
- * features/tokenusage/contexts/TokenUsageAdminContext.tsx).
15
+ * The sections every NJA project has, by key. Hrefs are constants rather than
16
+ * props: the path convention IS the contract that makes this component
17
+ * reusable. Exported so an app that passes its own `groups` places these
18
+ * entries without retyping their keys, icons and copy.
19
+ *
20
+ * `billing` is the only conditional entry: the default platform group shows it
21
+ * only when Stripe is configured (see AdminIndexGrid). An app that places it in
22
+ * its own `groups` applies the same check itself.
16
23
  */
17
- const PLATFORM_SECTIONS: AdminSection[] = [
18
- {
24
+ export const ADMIN_PLATFORM_SECTIONS: Record<PlatformSectionKey, AdminSection> = {
25
+ companies: {
19
26
  key: "companies",
20
27
  icon: Building2Icon,
21
28
  labelKey: "entities.companies",
@@ -23,7 +30,7 @@ const PLATFORM_SECTIONS: AdminSection[] = [
23
30
  descriptionKey: "administration.companies.description",
24
31
  href: "/administration/companies",
25
32
  },
26
- {
33
+ users: {
27
34
  key: "users",
28
35
  icon: UsersIcon,
29
36
  labelKey: "entities.users",
@@ -31,21 +38,21 @@ const PLATFORM_SECTIONS: AdminSection[] = [
31
38
  descriptionKey: "administration.users.description",
32
39
  href: "/administration/users",
33
40
  },
34
- {
41
+ "token-usage": {
35
42
  key: "token-usage",
36
43
  icon: CoinsIcon,
37
44
  labelKey: "token_usage.admin.title",
38
45
  descriptionKey: "administration.token_usage.description",
39
46
  href: "/administration/token-usage",
40
47
  },
41
- {
48
+ "ai-connections": {
42
49
  key: "ai-connections",
43
50
  icon: PlugZapIcon,
44
51
  labelKey: "ai_connections.admin.title",
45
52
  descriptionKey: "administration.ai_connections.description",
46
53
  href: "/administration/ai-connections",
47
54
  },
48
- {
55
+ rbac: {
49
56
  key: "rbac",
50
57
  icon: ShieldCheckIcon,
51
58
  labelKey: "entities.rbac",
@@ -53,27 +60,33 @@ const PLATFORM_SECTIONS: AdminSection[] = [
53
60
  descriptionKey: "administration.rbac.description",
54
61
  href: "/administration/rbac",
55
62
  },
56
- ];
57
-
58
- /**
59
- * The only conditional entry. getStripePublishableKey comes from /client rather
60
- * than isStripeConfigured() from /billing on purpose: /billing exists to keep
61
- * Stripe.js out of the global bundle, and isStripeConfigured lives in
62
- * StripeProvider.tsx, which imports @stripe/stripe-js at module scope. The two
63
- * are the same check — isStripeConfigured is a one-line wrapper over this call.
64
- */
65
- const BILLING_SECTION: AdminSection = {
66
- key: "billing",
67
- icon: CreditCardIcon,
68
- labelKey: "billing.admin.products.title",
69
- descriptionKey: "administration.billing.description",
70
- href: "/administration/products",
63
+ billing: {
64
+ key: "billing",
65
+ icon: CreditCardIcon,
66
+ labelKey: "billing.admin.products.title",
67
+ descriptionKey: "administration.billing.description",
68
+ href: "/administration/products",
69
+ },
71
70
  };
72
71
 
73
- /** Keys of the built-in platform sections, for `hiddenSections`. */
74
- export type PlatformSectionKey = "companies" | "users" | "token-usage" | "ai-connections" | "rbac" | "billing";
72
+ /** The unconditional platform entries, in the default group's order. */
73
+ const PLATFORM_SECTIONS: AdminSection[] = [
74
+ ADMIN_PLATFORM_SECTIONS.companies,
75
+ ADMIN_PLATFORM_SECTIONS.users,
76
+ ADMIN_PLATFORM_SECTIONS["token-usage"],
77
+ ADMIN_PLATFORM_SECTIONS["ai-connections"],
78
+ ADMIN_PLATFORM_SECTIONS.rbac,
79
+ ];
75
80
 
76
81
  type AdminIndexProps = {
82
+ /**
83
+ * Replaces the default groups (the built-in platform group plus
84
+ * `additionalGroups`) with exactly these, in this order. For an app that
85
+ * arranges its administration by its own headings; it places the platform
86
+ * entries it wants from ADMIN_PLATFORM_SECTIONS. When set, `additionalGroups`
87
+ * is ignored and the Stripe check on `billing` is the app's to apply.
88
+ */
89
+ groups?: AdminGroup[];
77
90
  /** Project-specific groups, appended after the built-in platform group. */
78
91
  additionalGroups?: AdminGroup[];
79
92
  /**
@@ -90,10 +103,12 @@ type AdminIndexProps = {
90
103
  * The grid itself, with no page shell — this is what the specs render, because
91
104
  * RoundPageContainer pulls in the app Header and its four header contexts.
92
105
  */
93
- export function AdminIndexGrid({ additionalGroups = [], hiddenSections = [] }: AdminIndexProps) {
106
+ export function AdminIndexGrid({ groups: customGroups, additionalGroups = [], hiddenSections = [] }: AdminIndexProps) {
94
107
  const t = useTranslations();
95
108
 
96
- const available = getStripePublishableKey() ? [...PLATFORM_SECTIONS, BILLING_SECTION] : PLATFORM_SECTIONS;
109
+ const available = getStripePublishableKey()
110
+ ? [...PLATFORM_SECTIONS, ADMIN_PLATFORM_SECTIONS.billing]
111
+ : PLATFORM_SECTIONS;
97
112
 
98
113
  const platformGroup: AdminGroup = {
99
114
  key: "platform",
@@ -101,7 +116,16 @@ export function AdminIndexGrid({ additionalGroups = [], hiddenSections = [] }: A
101
116
  sections: available.filter((section) => !hiddenSections.includes(section.key as PlatformSectionKey)),
102
117
  };
103
118
 
104
- const groups = [platformGroup, ...additionalGroups].filter((group) => group.sections.length > 0);
119
+ // Custom groups still honour hiddenSections, so a project that cannot serve
120
+ // a platform route drops it the same way in either mode.
121
+ const groups = (
122
+ customGroups
123
+ ? customGroups.map((group) => ({
124
+ ...group,
125
+ sections: group.sections.filter((section) => !hiddenSections.includes(section.key as PlatformSectionKey)),
126
+ }))
127
+ : [platformGroup, ...additionalGroups]
128
+ ).filter((group) => group.sections.length > 0);
105
129
 
106
130
  return (
107
131
  <div className="mx-auto flex w-full max-w-5xl flex-1 flex-col gap-8 py-8">
@@ -146,11 +170,15 @@ export function AdminIndexGrid({ additionalGroups = [], hiddenSections = [] }: A
146
170
  * and its own page chrome (AdministrationProvider), so it does not inherit the
147
171
  * title and breadcrumb of whatever page rendered before it.
148
172
  */
149
- export function AdminIndexContainer({ additionalGroups, hiddenSections }: AdminIndexProps) {
173
+ export function AdminIndexContainer({ groups, additionalGroups, hiddenSections }: AdminIndexProps) {
174
+ const t = useTranslations();
175
+
176
+ // The index is the "Administration" page itself, so it names itself in the
177
+ // breadcrumb like every administration page does ("Home > Companies").
150
178
  return (
151
- <AdministrationProvider>
179
+ <AdministrationProvider section={t("administration.title")}>
152
180
  <RoundPageContainer>
153
- <AdminIndexGrid additionalGroups={additionalGroups} hiddenSections={hiddenSections} />
181
+ <AdminIndexGrid groups={groups} additionalGroups={additionalGroups} hiddenSections={hiddenSections} />
154
182
  </RoundPageContainer>
155
183
  </AdministrationProvider>
156
184
  );
@@ -3,7 +3,7 @@ import { BookOpenIcon } from "lucide-react";
3
3
  import { beforeEach, describe, expect, it } from "vitest";
4
4
  import { configureJsonApi } from "../../../../client/config";
5
5
  import { configureI18n } from "../../../../i18n";
6
- import { AdminIndexGrid } from "../AdminIndexContainer";
6
+ import { ADMIN_PLATFORM_SECTIONS, AdminIndexGrid } from "../AdminIndexContainer";
7
7
  import type { AdminGroup } from "../../data/admin-section.types";
8
8
 
9
9
  // next-intl is globally mocked in vitest.setup.ts with `t = (key) => key`, so
@@ -93,4 +93,64 @@ describe("AdminIndexGrid", () => {
93
93
  // RoundPageContainerTitle, so the grid must not repeat it.
94
94
  expect(screen.queryByText("administration.title")).not.toBeInTheDocument();
95
95
  });
96
+
97
+ describe("groups", () => {
98
+ const customGroups: AdminGroup[] = [
99
+ {
100
+ key: "customers",
101
+ labelKey: "administration.group.customers",
102
+ sections: [ADMIN_PLATFORM_SECTIONS.users, ADMIN_PLATFORM_SECTIONS.companies],
103
+ },
104
+ extraGroup,
105
+ {
106
+ key: "system",
107
+ labelKey: "administration.group.system",
108
+ sections: [ADMIN_PLATFORM_SECTIONS["ai-connections"], ADMIN_PLATFORM_SECTIONS.rbac],
109
+ },
110
+ ];
111
+
112
+ it("replaces the platform group and additionalGroups with exactly the given groups, in order", () => {
113
+ render(<AdminIndexGrid groups={customGroups} additionalGroups={[extraGroup]} />);
114
+
115
+ const headings = screen.getAllByRole("heading", { level: 3 }).map((h) => h.textContent);
116
+ expect(headings).toEqual([
117
+ "administration.group.customers",
118
+ "administration.group.documentation",
119
+ "administration.group.system",
120
+ ]);
121
+ expect(screen.queryByText("administration.group.platform")).not.toBeInTheDocument();
122
+ expect(screen.queryByTestId("admin-index-token-usage")).not.toBeInTheDocument();
123
+ expect(screen.getAllByTestId("admin-index-howtos")).toHaveLength(1);
124
+ });
125
+
126
+ it("renders sections in the order each group lists them", () => {
127
+ render(<AdminIndexGrid groups={customGroups} />);
128
+
129
+ const ids = screen.getAllByRole("link").map((link) => link.getAttribute("data-testid"));
130
+ expect(ids).toEqual([
131
+ "admin-index-users",
132
+ "admin-index-companies",
133
+ "admin-index-howtos",
134
+ "admin-index-ai-connections",
135
+ "admin-index-rbac",
136
+ ]);
137
+ expect(screen.getByTestId("admin-index-companies")).toHaveAttribute("href", "/administration/companies");
138
+ });
139
+
140
+ it("applies hiddenSections inside the given groups and skips a group left empty", () => {
141
+ render(
142
+ <AdminIndexGrid
143
+ groups={[
144
+ ...customGroups,
145
+ { key: "only-rbac", labelKey: "administration.group.only_rbac", sections: [ADMIN_PLATFORM_SECTIONS.rbac] },
146
+ ]}
147
+ hiddenSections={["rbac"]}
148
+ />,
149
+ );
150
+
151
+ expect(screen.queryByTestId("admin-index-rbac")).not.toBeInTheDocument();
152
+ expect(screen.getByTestId("admin-index-ai-connections")).toBeInTheDocument();
153
+ expect(screen.queryByText("administration.group.only_rbac")).not.toBeInTheDocument();
154
+ });
155
+ });
96
156
  });
@@ -1 +1,2 @@
1
- export { AdminIndexContainer, AdminIndexGrid } from "./AdminIndexContainer";
1
+ export { ADMIN_PLATFORM_SECTIONS, AdminIndexContainer, AdminIndexGrid } from "./AdminIndexContainer";
2
+ export type { PlatformSectionKey } from "./AdminIndexContainer";
@@ -11,6 +11,14 @@ const AdministrationContext = createContext<AdministrationContextType | undefine
11
11
 
12
12
  type AdministrationProviderProps = {
13
13
  children: ReactNode;
14
+ /**
15
+ * The page's own name, already translated (the same label the sidebar and the
16
+ * administration index use for it). When given, the breadcrumb reads
17
+ * "Home > {section}" with no link (it is the current page) and the title bar
18
+ * shows it. A plain string so a Server Component page can pass an already translated label.
19
+ * Omitted, the breadcrumb is empty and the title is "Administration".
20
+ */
21
+ section?: string;
14
22
  };
15
23
 
16
24
  /**
@@ -26,13 +34,13 @@ type AdministrationProviderProps = {
26
34
  * Mirrors CommonProvider (contexts/CommonContext.tsx), which does the same job
27
35
  * for the ordinary application shell.
28
36
  */
29
- export const AdministrationProvider = ({ children }: AdministrationProviderProps) => {
37
+ export const AdministrationProvider = ({ children, section }: AdministrationProviderProps) => {
30
38
  const t = useTranslations();
31
39
 
32
- const breadcrumbs: BreadcrumbItemData[] = [];
40
+ const breadcrumbs: BreadcrumbItemData[] = section ? [{ name: section }] : [];
33
41
 
34
42
  return (
35
- <SharedProvider value={{ breadcrumbs, title: { type: t(`administration.title`) } }}>
43
+ <SharedProvider value={{ breadcrumbs, title: { type: section ?? t(`administration.title`) } }}>
36
44
  <AdministrationContext.Provider value={{}}>{children}</AdministrationContext.Provider>
37
45
  </SharedProvider>
38
46
  );
@@ -0,0 +1,40 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { useSharedContext } from "../../../../contexts/SharedContext";
4
+ import { AdministrationProvider } from "../AdministrationContext";
5
+
6
+ // next-intl is globally mocked in vitest.setup.ts with `t = (key) => key`, so a
7
+ // translated label renders as its own key.
8
+ const SharedProbe = () => {
9
+ const { breadcrumbs, title } = useSharedContext();
10
+ return (
11
+ <>
12
+ <span data-testid="breadcrumbs">{JSON.stringify(breadcrumbs)}</span>
13
+ <span data-testid="title">{String(title.type)}</span>
14
+ </>
15
+ );
16
+ };
17
+
18
+ describe("AdministrationProvider", () => {
19
+ it("publishes an empty breadcrumb and the administration title when no section is given", () => {
20
+ render(
21
+ <AdministrationProvider>
22
+ <SharedProbe />
23
+ </AdministrationProvider>,
24
+ );
25
+
26
+ expect(screen.getByTestId("breadcrumbs")).toHaveTextContent("[]");
27
+ expect(screen.getByTestId("title")).toHaveTextContent("administration.title");
28
+ });
29
+
30
+ it("publishes the section as an unlinked breadcrumb and as the title", () => {
31
+ render(
32
+ <AdministrationProvider section="Calendars">
33
+ <SharedProbe />
34
+ </AdministrationProvider>,
35
+ );
36
+
37
+ expect(screen.getByTestId("breadcrumbs")).toHaveTextContent(JSON.stringify([{ name: "Calendars" }]));
38
+ expect(screen.getByTestId("title")).toHaveTextContent("Calendars");
39
+ });
40
+ });
@@ -1,3 +1,4 @@
1
+ import { PlugZapIcon } from "lucide-react";
1
2
  import { ModuleFactory } from "../../permissions";
2
3
  import { AiConnection } from "./data";
3
4
 
@@ -11,4 +12,5 @@ export const AiConnectionModule = (factory: ModuleFactory) =>
11
12
  pageUrl: "/administration/ai-connections",
12
13
  model: AiConnection,
13
14
  moduleId: "7d3f2b8a-91c4-4e6d-a5f0-2c9b8e4d1a37",
15
+ icon: PlugZapIcon,
14
16
  });
@@ -3,18 +3,9 @@
3
3
  import { useTranslations } from "next-intl";
4
4
  import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
5
5
  import { SharedProvider } from "../../../contexts";
6
- import { usePageUrlGenerator } from "../../../hooks";
7
6
  import { BreadcrumbItemData } from "../../../interfaces";
8
7
  import { AiConnectionEnvDefaults, AiConnectionInterface, AiConnectionService, AiProviderRegistry } from "../data";
9
8
 
10
- /**
11
- * Route the breadcrumb links back to. Constant rather than a prop for the same
12
- * reason TOKEN_USAGE_ADMIN_PAGE_URL is
13
- * (features/tokenusage/contexts/TokenUsageAdminContext.tsx): the breadcrumb is
14
- * built here, not by the consuming app, and the path convention IS the contract.
15
- */
16
- export const AI_CONNECTIONS_ADMIN_PAGE_URL = "/administration/ai-connections";
17
-
18
9
  export interface AiConnectionsContextType {
19
10
  /** Every connection, both scopes — the cards filter per scope themselves. */
20
11
  connections: AiConnectionInterface[];
@@ -57,7 +48,6 @@ export type AiConnectionsProviderProps = {
57
48
  */
58
49
  export const AiConnectionsProvider = ({ children }: AiConnectionsProviderProps) => {
59
50
  const t = useTranslations();
60
- const generateUrl = usePageUrlGenerator();
61
51
 
62
52
  const [connections, setConnections] = useState<AiConnectionInterface[]>([]);
63
53
  const [providerRegistry, setProviderRegistry] = useState<AiProviderRegistry>({});
@@ -94,12 +84,9 @@ export const AiConnectionsProvider = ({ children }: AiConnectionsProviderProps)
94
84
  [connections, providerRegistry, envDefaults, scopeCompanyId, refresh, isLoading],
95
85
  );
96
86
 
97
- const breadcrumbs: BreadcrumbItemData[] = [
98
- {
99
- name: t("ai_connections.admin.title"),
100
- href: generateUrl({ page: AI_CONNECTIONS_ADMIN_PAGE_URL }),
101
- },
102
- ];
87
+ // Unlinked: this provider only serves the AI-connections page, so the root is
88
+ // always the current page.
89
+ const breadcrumbs: BreadcrumbItemData[] = [{ name: t("ai_connections.admin.title") }];
103
90
 
104
91
  return (
105
92
  <SharedProvider value={{ breadcrumbs, title: { type: t("ai_connections.admin.title") } }}>
@@ -0,0 +1,33 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { useSharedContext } from "../../../../contexts/SharedContext";
4
+ import { AiConnectionService } from "../../data";
5
+ import { AiConnectionsProvider } from "../AiConnectionsContext";
6
+
7
+ vi.mock("../../data", () => ({
8
+ AiConnectionService: {
9
+ listConnections: vi.fn(async () => ({ connections: [], providerRegistry: {}, envDefaults: {} })),
10
+ },
11
+ }));
12
+
13
+ // next-intl is globally mocked in vitest.setup.ts with `t = (key) => key`, so a
14
+ // translated label renders as its own key.
15
+ function BreadcrumbProbe() {
16
+ const { breadcrumbs } = useSharedContext();
17
+ return <span data-testid="breadcrumbs">{JSON.stringify(breadcrumbs)}</span>;
18
+ }
19
+
20
+ describe("AiConnectionsContext", () => {
21
+ it("publishes the page as an unlinked breadcrumb, since it is the current page", async () => {
22
+ render(
23
+ <AiConnectionsProvider>
24
+ <BreadcrumbProbe />
25
+ </AiConnectionsProvider>,
26
+ );
27
+
28
+ await waitFor(() => expect(AiConnectionService.listConnections).toHaveBeenCalled());
29
+ expect(screen.getByTestId("breadcrumbs")).toHaveTextContent(
30
+ JSON.stringify([{ name: "ai_connections.admin.title" }]),
31
+ );
32
+ });
33
+ });
@@ -35,7 +35,8 @@ describe("analytics server entry", () => {
35
35
 
36
36
  expect(mocks.hasRole).toHaveBeenCalledWith("role-admin");
37
37
  expect(element.type).toBe(AnalyticsAdminPageContainer);
38
- expect(element.props).toEqual({ pageUrl: "/admin/stats", sessionPageUrl: "/admin/stats/sessions" });
38
+ // `pageUrl` is deprecated and no longer forwarded: the dashboard breadcrumb is unlinked.
39
+ expect(element.props).toEqual({ sessionPageUrl: "/admin/stats/sessions" });
39
40
  });
40
41
 
41
42
  it("AnalyticsAdminPage redirects to `/` by default without the role", async () => {
@@ -1,3 +1,4 @@
1
+ import { ChartNoAxesCombinedIcon } from "lucide-react";
1
2
  import { ModuleFactory, ModuleWithPermissions } from "../../permissions";
2
3
  import { AnalyticsBreakdown } from "./data/analytics-breakdown";
3
4
  import { AnalyticsEvent } from "./data/analytics-event";
@@ -22,9 +23,25 @@ import { AnalyticsTimeline } from "./data/analytics-timeline";
22
23
  export const analyticsModules = (factory: ModuleFactory) =>
23
24
  ({
24
25
  AnalyticsEvent: factory({ name: "analytics/events", model: AnalyticsEvent }),
25
- AnalyticsSummary: factory({ name: "analytics/administration/summary", model: AnalyticsSummary }),
26
- AnalyticsTimeline: factory({ name: "analytics/administration/timeline", model: AnalyticsTimeline }),
27
- AnalyticsBreakdown: factory({ name: "analytics/administration/breakdown", model: AnalyticsBreakdown }),
28
- AnalyticsSession: factory({ name: "analytics/administration/sessions", model: AnalyticsSession }),
29
- AnalyticsPageView: factory({ name: "pageviews", model: AnalyticsPageView }),
26
+ AnalyticsSummary: factory({
27
+ name: "analytics/administration/summary",
28
+ model: AnalyticsSummary,
29
+ icon: ChartNoAxesCombinedIcon,
30
+ }),
31
+ AnalyticsTimeline: factory({
32
+ name: "analytics/administration/timeline",
33
+ model: AnalyticsTimeline,
34
+ icon: ChartNoAxesCombinedIcon,
35
+ }),
36
+ AnalyticsBreakdown: factory({
37
+ name: "analytics/administration/breakdown",
38
+ model: AnalyticsBreakdown,
39
+ icon: ChartNoAxesCombinedIcon,
40
+ }),
41
+ AnalyticsSession: factory({
42
+ name: "analytics/administration/sessions",
43
+ model: AnalyticsSession,
44
+ icon: ChartNoAxesCombinedIcon,
45
+ }),
46
+ AnalyticsPageView: factory({ name: "pageviews", model: AnalyticsPageView, icon: ChartNoAxesCombinedIcon }),
30
47
  }) satisfies Record<string, ModuleWithPermissions>;
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useTranslations } from "next-intl";
4
4
  import { RoundPageContainer } from "../../../components";
5
+ import { Modules } from "../../../core";
5
6
  import { Card, CardContent, CardHeader, CardTitle } from "../../../shadcnui";
6
7
  import { useAnalyticsAdmin } from "../contexts/AnalyticsAdminContext";
7
8
  import { AnalyticsAdminTiles } from "./AnalyticsAdminTiles";
@@ -33,7 +34,7 @@ export function AnalyticsAdminContainer() {
33
34
 
34
35
  if (error) {
35
36
  return (
36
- <RoundPageContainer fullWidth forceHeader>
37
+ <RoundPageContainer module={Modules.AnalyticsSummary} fullWidth forceHeader>
37
38
  <div className="p-4">
38
39
  <Card>
39
40
  <CardContent>
@@ -47,10 +48,10 @@ export function AnalyticsAdminContainer() {
47
48
 
48
49
  // Loading renders nothing in the body: the title bar (with the filter bar) is
49
50
  // already mounted, so a spinner would only make the controls jump on arrival.
50
- if (isLoading) return <RoundPageContainer fullWidth forceHeader />;
51
+ if (isLoading) return <RoundPageContainer module={Modules.AnalyticsSummary} fullWidth forceHeader />;
51
52
 
52
53
  return (
53
- <RoundPageContainer fullWidth forceHeader>
54
+ <RoundPageContainer module={Modules.AnalyticsSummary} fullWidth forceHeader>
54
55
  <div className="flex w-full flex-col gap-4 p-4">
55
56
  <div className="flex flex-col gap-2">
56
57
  <AnalyticsAdminTiles summary={summary} />
@@ -4,7 +4,10 @@ import { AnalyticsAdminProvider } from "../contexts/AnalyticsAdminContext";
4
4
  import { AnalyticsAdminContainer } from "./AnalyticsAdminContainer";
5
5
 
6
6
  type Props = {
7
- /** Route the breadcrumb links back to. Defaults to the provider's default. */
7
+ /**
8
+ * @deprecated No longer read: the dashboard is the current page, so its
9
+ * breadcrumb is not linked. Kept so existing callers still compile.
10
+ */
8
11
  pageUrl?: string;
9
12
  /** Base route of the journey page each session row links to. */
10
13
  sessionPageUrl?: string;
@@ -16,9 +19,9 @@ type Props = {
16
19
  * the `RoundPageContainer`; the provider sits above it because it publishes the
17
20
  * filter bar into the title bar.
18
21
  */
19
- export function AnalyticsAdminPageContainer({ pageUrl, sessionPageUrl }: Props) {
22
+ export function AnalyticsAdminPageContainer({ sessionPageUrl }: Props) {
20
23
  return (
21
- <AnalyticsAdminProvider pageUrl={pageUrl} sessionPageUrl={sessionPageUrl}>
24
+ <AnalyticsAdminProvider sessionPageUrl={sessionPageUrl}>
22
25
  <AnalyticsAdminContainer />
23
26
  </AnalyticsAdminProvider>
24
27
  );
@@ -4,6 +4,7 @@ import { useLocale, useTranslations } from "next-intl";
4
4
  import { useEffect, useMemo, useState } from "react";
5
5
  import { DetailField, RoundPageContainer } from "../../../components";
6
6
  import { SharedProvider } from "../../../contexts";
7
+ import { Modules } from "../../../core";
7
8
  import { usePageUrlGenerator } from "../../../hooks";
8
9
  import { BreadcrumbItemData } from "../../../interfaces";
9
10
  import {
@@ -125,7 +126,7 @@ export function AnalyticsJourneyContainer({ sessionId, pageUrl = ANALYTICS_ADMIN
125
126
  const renderBody = () => {
126
127
  if (error) {
127
128
  return (
128
- <RoundPageContainer fullWidth forceHeader>
129
+ <RoundPageContainer module={Modules.AnalyticsSession} fullWidth forceHeader>
129
130
  <div className="p-4">
130
131
  <Card>
131
132
  <CardContent>
@@ -138,14 +139,14 @@ export function AnalyticsJourneyContainer({ sessionId, pageUrl = ANALYTICS_ADMIN
138
139
  }
139
140
 
140
141
  // Loading renders nothing in the body, as on the dashboard.
141
- if (isLoading || !session) return <RoundPageContainer fullWidth forceHeader />;
142
+ if (isLoading || !session) return <RoundPageContainer module={Modules.AnalyticsSession} fullWidth forceHeader />;
142
143
 
143
144
  const source = [session.utmSource ?? session.referrerHost, session.utmMedium, session.utmCampaign]
144
145
  .filter(Boolean)
145
146
  .join(" / ");
146
147
 
147
148
  return (
148
- <RoundPageContainer fullWidth forceHeader>
149
+ <RoundPageContainer module={Modules.AnalyticsSession} fullWidth forceHeader>
149
150
  <div className="flex w-full flex-col gap-4 p-4">
150
151
  <Card data-testid="analytics-journey-attribution">
151
152
  <CardContent className="flex flex-col gap-3">
@@ -22,6 +22,17 @@ vi.mock("../../../../components", async (importOriginal) => {
22
22
  };
23
23
  });
24
24
 
25
+ // The container hands `Modules.AnalyticsSession` to the (mocked) page chrome for
26
+ // its header icon; no app bootstrap registers modules here, so the registry
27
+ // lookup would throw.
28
+ vi.mock("../../../../core", async (importOriginal) => {
29
+ const actual = await importOriginal<typeof import("../../../../core")>();
30
+ return {
31
+ ...actual,
32
+ Modules: new Proxy({}, { get: () => undefined }),
33
+ };
34
+ });
35
+
25
36
  beforeAll(() => {
26
37
  configureI18n({
27
38
  useRouter: () => ({
@@ -27,6 +27,17 @@ vi.mock("../../../../components", async (importOriginal) => {
27
27
  };
28
28
  });
29
29
 
30
+ // The containers pass Modules.AnalyticsSummary / AnalyticsSession for the
31
+ // header icon; no app bootstrap registers modules here, so the registry lookup
32
+ // would throw.
33
+ vi.mock("../../../../core", async (importOriginal) => {
34
+ const actual = await importOriginal<typeof import("../../../../core")>();
35
+ return {
36
+ ...actual,
37
+ Modules: new Proxy({}, { get: () => undefined }),
38
+ };
39
+ });
40
+
30
41
  beforeAll(() => {
31
42
  configureI18n({
32
43
  useRouter: () => ({
@@ -81,7 +92,7 @@ describe("AnalyticsAdminPageContainer", () => {
81
92
  });
82
93
 
83
94
  it("loads the dashboard and links each session under the given sessionPageUrl", async () => {
84
- render(<AnalyticsAdminPageContainer pageUrl="/admin/stats" sessionPageUrl="/admin/stats/sessions" />);
95
+ render(<AnalyticsAdminPageContainer sessionPageUrl="/admin/stats/sessions" />);
85
96
 
86
97
  expect(await screen.findByTestId("analytics-session-link-s1")).toHaveAttribute("href", "/admin/stats/sessions/s1");
87
98
  expect(AnalyticsAdminService.getSummary).toHaveBeenCalledTimes(1);
@@ -10,7 +10,10 @@ import { ServerSession } from "../../../../server";
10
10
  type Props = {
11
11
  /** Role id the viewer must hold. The package has no role constants, so the host app passes its own. */
12
12
  adminRoleId: string;
13
- /** Route the breadcrumb links back to. */
13
+ /**
14
+ * @deprecated No longer read: the dashboard is the current page, so its
15
+ * breadcrumb is not linked. Kept so existing callers still compile.
16
+ */
14
17
  pageUrl?: string;
15
18
  /** Base route of the journey page each session row links to. */
16
19
  sessionPageUrl?: string;
@@ -23,10 +26,10 @@ type Props = {
23
26
  *
24
27
  * Reads the session: render inside a `<Suspense>` boundary (Cache Components).
25
28
  */
26
- export async function AnalyticsAdminPage({ adminRoleId, pageUrl, sessionPageUrl, deniedRedirect = "/" }: Props) {
29
+ export async function AnalyticsAdminPage({ adminRoleId, sessionPageUrl, deniedRedirect = "/" }: Props) {
27
30
  if (!(await ServerSession.hasRole(adminRoleId))) {
28
31
  redirect(deniedRedirect);
29
32
  }
30
33
 
31
- return <AnalyticsAdminPageContainer pageUrl={pageUrl} sessionPageUrl={sessionPageUrl} />;
34
+ return <AnalyticsAdminPageContainer sessionPageUrl={sessionPageUrl} />;
32
35
  }