@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.
- package/dist/{BlockNoteEditor-4YCAIEPT.js → BlockNoteEditor-AE3ZTEEH.js} +20 -20
- package/dist/{BlockNoteEditor-4YCAIEPT.js.map → BlockNoteEditor-AE3ZTEEH.js.map} +1 -1
- package/dist/{BlockNoteEditor-YQL2SGFR.mjs → BlockNoteEditor-NEYG5L6W.mjs} +5 -5
- package/dist/{BlockNoteViewer-AEJKJ27Q.mjs → BlockNoteViewer-3OUT5UFY.mjs} +3 -3
- package/dist/{BlockNoteViewer-YL3J7J2Y.js → BlockNoteViewer-4CFKQM7U.js} +4 -4
- package/dist/{BlockNoteViewer-YL3J7J2Y.js.map → BlockNoteViewer-4CFKQM7U.js.map} +1 -1
- package/dist/billing/index.d.mts +186 -25
- package/dist/billing/index.d.ts +186 -25
- package/dist/billing/index.js +1149 -588
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +786 -225
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-5PPG6PL7.js → chunk-3I5AAJXV.js} +3 -3
- package/dist/{chunk-5PPG6PL7.js.map → chunk-3I5AAJXV.js.map} +1 -1
- package/dist/{chunk-YW4ZCEWB.js → chunk-53BAULDP.js} +4 -4
- package/dist/{chunk-YW4ZCEWB.js.map → chunk-53BAULDP.js.map} +1 -1
- package/dist/{chunk-XCHSX3VC.mjs → chunk-BTPUYUQ3.mjs} +2 -2
- package/dist/{chunk-WC63PJSC.mjs → chunk-EYA2F6AN.mjs} +756 -24
- package/dist/chunk-EYA2F6AN.mjs.map +1 -0
- package/dist/{chunk-NFBOSPNJ.js → chunk-HKZAESS6.js} +954 -222
- package/dist/chunk-HKZAESS6.js.map +1 -0
- package/dist/{chunk-6DC2WVVB.mjs → chunk-KQHBONL4.mjs} +2 -2
- package/dist/{chunk-3NLAXVIN.js → chunk-LQ4DK4JF.js} +1988 -1089
- package/dist/chunk-LQ4DK4JF.js.map +1 -0
- package/dist/{chunk-YNCQSWQW.js → chunk-RL3PL3KV.js} +8 -8
- package/dist/{chunk-YNCQSWQW.js.map → chunk-RL3PL3KV.js.map} +1 -1
- package/dist/{chunk-EUKTBOI2.mjs → chunk-SFYPXCK4.mjs} +3382 -2483
- package/dist/chunk-SFYPXCK4.mjs.map +1 -0
- package/dist/{chunk-QMYQRPQC.mjs → chunk-Y7RR57EW.mjs} +2 -2
- package/dist/client/index.js +5 -5
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +4 -4
- package/dist/components/editors/server.d.mts +15 -0
- package/dist/components/editors/server.d.ts +15 -0
- package/dist/components/editors/server.js +38 -0
- package/dist/components/editors/server.js.map +1 -0
- package/dist/components/editors/server.mjs +38 -0
- package/dist/components/editors/server.mjs.map +1 -0
- package/dist/components/index.d.mts +33 -10
- package/dist/components/index.d.ts +33 -10
- package/dist/components/index.js +7 -5
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +8 -6
- package/dist/contexts/index.d.mts +9 -1
- package/dist/contexts/index.d.ts +9 -1
- package/dist/contexts/index.js +5 -5
- package/dist/contexts/index.mjs +4 -4
- package/dist/core/index.d.mts +242 -3
- package/dist/core/index.d.ts +242 -3
- package/dist/core/index.js +36 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +35 -3
- package/dist/email-automations/index.d.mts +3 -0
- package/dist/email-automations/index.d.ts +3 -0
- package/dist/email-automations/index.js +99 -95
- package/dist/email-automations/index.js.map +1 -1
- package/dist/email-automations/index.mjs +30 -26
- package/dist/email-automations/index.mjs.map +1 -1
- package/dist/features/analytics/index.d.mts +11 -4
- package/dist/features/analytics/index.d.ts +11 -4
- package/dist/features/analytics/index.js +127 -135
- package/dist/features/analytics/index.js.map +1 -1
- package/dist/features/analytics/index.mjs +16 -24
- package/dist/features/analytics/index.mjs.map +1 -1
- package/dist/features/analytics/server.d.mts +5 -2
- package/dist/features/analytics/server.d.ts +5 -2
- package/dist/features/analytics/server.js +7 -7
- package/dist/features/analytics/server.js.map +1 -1
- package/dist/features/analytics/server.mjs +5 -5
- package/dist/features/analytics/server.mjs.map +1 -1
- package/dist/features/blog/index.d.mts +12 -2
- package/dist/features/blog/index.d.ts +12 -2
- package/dist/features/blog/index.js +206 -195
- package/dist/features/blog/index.js.map +1 -1
- package/dist/features/blog/index.mjs +71 -60
- package/dist/features/blog/index.mjs.map +1 -1
- package/dist/features/blog/server.d.mts +4 -1
- package/dist/features/blog/server.d.ts +4 -1
- package/dist/features/blog/server.js +49 -21
- package/dist/features/blog/server.js.map +1 -1
- package/dist/features/blog/server.mjs +36 -8
- package/dist/features/blog/server.mjs.map +1 -1
- package/dist/features/help/index.js +38 -38
- package/dist/features/help/index.mjs +4 -4
- package/dist/features/tokenusage/index.d.mts +1 -3
- package/dist/features/tokenusage/index.d.ts +1 -3
- package/dist/features/tokenusage/index.js +127 -135
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +10 -18
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/{stripe-promotion-code.interface-C3qqh3mi.d.ts → format-options-BtX3o0Yu.d.ts} +104 -1
- package/dist/{stripe-promotion-code.interface-ClZ7DxS9.d.mts → format-options-D6c7FNDi.d.mts} +104 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +37 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +36 -4
- package/dist/server/index.js +4 -4
- package/dist/server/index.mjs +3 -3
- package/messages/en.json +125 -3
- package/messages/it.json +125 -3
- package/package.json +9 -1
- package/scripts/validate-messages.mjs +1 -0
- package/src/billing/index.ts +1 -0
- package/src/client/index.ts +4 -0
- package/src/components/editors/server/BlockNoteServerContent.tsx +28 -0
- package/src/components/editors/server/__tests__/renderBlockNoteHTML.spec.ts +44 -0
- package/src/components/editors/server/renderBlockNoteHTML.ts +26 -0
- package/src/components/editors/server-entry.ts +7 -0
- package/src/components/index.ts +1 -0
- package/src/components/viewers/BlockNoteStaticViewer.tsx +44 -0
- package/src/core/index.ts +1 -0
- package/src/core/registry/ModuleRegistry.ts +3 -0
- package/src/features/administration/components/AdminIndexContainer.tsx +61 -33
- package/src/features/administration/components/__tests__/AdminIndexContainer.spec.tsx +61 -1
- package/src/features/administration/components/index.ts +2 -1
- package/src/features/administration/contexts/AdministrationContext.tsx +11 -3
- package/src/features/administration/contexts/__tests__/AdministrationContext.spec.tsx +40 -0
- package/src/features/ai-connection/ai-connection.module.ts +2 -0
- package/src/features/ai-connection/contexts/AiConnectionsContext.tsx +3 -16
- package/src/features/ai-connection/contexts/__tests__/AiConnectionsContext.spec.tsx +33 -0
- package/src/features/analytics/__tests__/server-entry.spec.tsx +2 -1
- package/src/features/analytics/analytics.modules.ts +22 -5
- package/src/features/analytics/components/AnalyticsAdminContainer.tsx +4 -3
- package/src/features/analytics/components/AnalyticsAdminPageContainer.tsx +6 -3
- package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +4 -3
- package/src/features/analytics/components/__tests__/AnalyticsJourneyContainer.spec.tsx +11 -0
- package/src/features/analytics/components/__tests__/AnalyticsPageContainers.spec.tsx +12 -1
- package/src/features/analytics/components/server/AnalyticsAdminPage.tsx +6 -3
- package/src/features/analytics/contexts/AnalyticsAdminContext.tsx +8 -17
- package/src/features/analytics/contexts/__tests__/AnalyticsAdminContext.spec.tsx +17 -0
- package/src/features/billing/components/cards/InvoicesSummaryCard.tsx +3 -2
- package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +4 -3
- package/src/features/billing/components/containers/BillingDashboardContainer.tsx +22 -5
- package/src/features/billing/components/utils/__tests__/discount.spec.ts +59 -0
- package/src/features/billing/components/utils/__tests__/url.spec.ts +32 -0
- package/src/features/billing/components/utils/discount.ts +38 -0
- package/src/features/billing/components/utils/index.ts +2 -0
- package/src/features/billing/components/utils/url.ts +12 -0
- package/src/features/billing/contexts/CouponContext.tsx +211 -0
- package/src/features/billing/contexts/index.ts +1 -0
- package/src/features/billing/i18n-keys.ts +79 -0
- package/src/features/billing/stripe-coupon/components/containers/CouponContainer.tsx +38 -0
- package/src/features/billing/stripe-coupon/components/containers/index.ts +1 -0
- package/src/features/billing/stripe-coupon/components/details/CouponDetails.tsx +69 -0
- package/src/features/billing/stripe-coupon/components/details/index.ts +1 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponCodeEditor.tsx +79 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponCodeFields.tsx +192 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponEditor.tsx +333 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponRefresher.tsx +45 -0
- package/src/features/billing/stripe-coupon/components/forms/index.ts +6 -0
- package/src/features/billing/stripe-coupon/components/index.ts +5 -0
- package/src/features/billing/stripe-coupon/components/lists/CouponCodesList.tsx +61 -0
- package/src/features/billing/stripe-coupon/components/lists/CouponsList.tsx +67 -0
- package/src/features/billing/stripe-coupon/components/lists/index.ts +2 -0
- package/src/features/billing/stripe-coupon/components/widgets/CouponCodeActions.tsx +80 -0
- package/src/features/billing/stripe-coupon/components/widgets/index.ts +1 -0
- package/src/features/billing/stripe-coupon/data/__tests__/stripe-coupon-models.spec.ts +114 -0
- package/src/features/billing/stripe-coupon/data/index.ts +10 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.fields.ts +8 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.interface.ts +44 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.service.ts +92 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.ts +132 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-offer.interface.ts +17 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-offer.ts +70 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.fields.ts +7 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.interface.ts +51 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.service.ts +93 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.ts +165 -0
- package/src/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.tsx +154 -0
- package/src/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.tsx +113 -0
- package/src/features/billing/stripe-coupon/index.ts +5 -0
- package/src/features/billing/stripe-coupon/stripe-coupon-code.module.ts +14 -0
- package/src/features/billing/stripe-coupon/stripe-coupon-offer.module.ts +10 -0
- package/src/features/billing/stripe-coupon/stripe-coupon.module.ts +18 -0
- package/src/features/billing/stripe-coupon/utils/__tests__/coupon-places.spec.ts +19 -0
- package/src/features/billing/stripe-coupon/utils/__tests__/to-promotion-code-result.spec.ts +41 -0
- package/src/features/billing/stripe-coupon/utils/coupon-places.ts +23 -0
- package/src/features/billing/stripe-coupon/utils/format-coupon-discount.ts +36 -0
- package/src/features/billing/stripe-coupon/utils/index.ts +3 -0
- package/src/features/billing/stripe-coupon/utils/to-promotion-code-result.ts +23 -0
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +24 -8
- package/src/features/billing/stripe-price/stripe-price.module.ts +2 -0
- package/src/features/billing/stripe-product/components/containers/ProductsListContainer.tsx +21 -6
- package/src/features/billing/stripe-product/stripe-product.module.ts +2 -0
- package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +60 -19
- package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.interface.ts +2 -0
- package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +24 -5
- package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +21 -2
- package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +1 -1
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +1 -1
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +60 -49
- package/src/features/billing/stripe-subscription/components/wizards/__tests__/review-card.spec.ts +34 -0
- package/src/features/billing/stripe-subscription/components/wizards/review-card.ts +24 -0
- package/src/features/billing/stripe-subscription/data/__tests__/stripe-subscription.discount.spec.ts +67 -0
- package/src/features/billing/stripe-subscription/data/stripe-subscription.interface.ts +14 -0
- package/src/features/billing/stripe-subscription/data/stripe-subscription.service.ts +39 -0
- package/src/features/billing/stripe-subscription/data/stripe-subscription.ts +21 -0
- package/src/features/billing/stripe-subscription/hooks/__tests__/wizard-payment.spec.ts +48 -0
- package/src/features/billing/stripe-subscription/hooks/__tests__/wizard-promotion.spec.ts +132 -0
- package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +138 -12
- package/src/features/billing/stripe-subscription/hooks/wizard-payment.ts +30 -0
- package/src/features/billing/stripe-subscription/hooks/wizard-promotion.ts +54 -0
- package/src/features/blog/components/admin/BlogAdminContainer.tsx +13 -6
- package/src/features/blog/components/admin/BlogCategoriesList.tsx +1 -1
- package/src/features/blog/components/admin/BlogCoverImageField.tsx +1 -1
- package/src/features/blog/components/admin/BlogPostContainer.tsx +0 -1
- package/src/features/blog/components/admin/BlogPostEditor.tsx +42 -33
- package/src/features/blog/components/admin/BlogPostTranslationEditor.tsx +12 -4
- package/src/features/blog/components/admin/BlogPostsList.tsx +1 -1
- package/src/features/blog/components/public/BlogPostCard.tsx +2 -9
- package/src/features/blog/components/public/BlogPostMeta.tsx +2 -2
- package/src/features/blog/components/public/BlogPostView.tsx +9 -3
- package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +2 -0
- package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +17 -11
- package/src/features/blog/server/fetchers.ts +57 -8
- package/src/features/email-automation/components/containers/EmailAutomationsListContainer.tsx +12 -3
- package/src/features/rbac/contexts/RbacContext.tsx +4 -7
- package/src/features/rbac/contexts/__tests__/RbacContext.spec.tsx +42 -0
- package/src/features/tokenusage/components/TokenUsageAdminContainer.tsx +4 -3
- package/src/features/tokenusage/contexts/TokenUsageAdminContext.tsx +3 -18
- package/src/features/tokenusage/contexts/__tests__/TokenUsageAdminContext.spec.tsx +17 -0
- package/src/features/tokenusage/tokenusage-admin.module.ts +4 -3
- package/src/features/tokenusage/tokenusage.modules.ts +12 -2
- package/src/features/user/contexts/CurrentUserContext.tsx +25 -0
- package/src/features/user/contexts/UserContext.tsx +18 -3
- package/src/features/user/contexts/__tests__/CurrentUserContext.spec.tsx +121 -2
- package/src/features/user/contexts/__tests__/UserContext.spec.tsx +95 -0
- package/dist/chunk-3NLAXVIN.js.map +0 -1
- package/dist/chunk-EUKTBOI2.mjs.map +0 -1
- package/dist/chunk-NFBOSPNJ.js.map +0 -1
- package/dist/chunk-WC63PJSC.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-YQL2SGFR.mjs.map → BlockNoteEditor-NEYG5L6W.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-AEJKJ27Q.mjs.map → BlockNoteViewer-3OUT5UFY.mjs.map} +0 -0
- /package/dist/{chunk-XCHSX3VC.mjs.map → chunk-BTPUYUQ3.mjs.map} +0 -0
- /package/dist/{chunk-6DC2WVVB.mjs.map → chunk-KQHBONL4.mjs.map} +0 -0
- /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
|
|
13
|
-
* the path convention IS the contract that makes this component
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
/**
|
|
74
|
-
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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({
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
/**
|
|
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({
|
|
22
|
+
export function AnalyticsAdminPageContainer({ sessionPageUrl }: Props) {
|
|
20
23
|
return (
|
|
21
|
-
<AnalyticsAdminProvider
|
|
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
|
|
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
|
-
/**
|
|
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,
|
|
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
|
|
34
|
+
return <AnalyticsAdminPageContainer sessionPageUrl={sessionPageUrl} />;
|
|
32
35
|
}
|