@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CoinsIcon } from "lucide-react";
|
|
1
2
|
import { ModuleFactory } from "../../permissions";
|
|
2
3
|
import { TokenUsageAdminBreakdown } from "./data/tokenusage-admin-breakdown";
|
|
3
4
|
import { TokenUsageAdminSummary } from "./data/tokenusage-admin-summary";
|
|
@@ -17,10 +18,10 @@ import { TokenUsageAdminTimeline } from "./data/tokenusage-admin-timeline";
|
|
|
17
18
|
* object. These individual factories are kept for backward compatibility.
|
|
18
19
|
*/
|
|
19
20
|
export const TokenUsageAdminSummaryModule = (factory: ModuleFactory) =>
|
|
20
|
-
factory({ name: "tokenusages/administration/summary", model: TokenUsageAdminSummary });
|
|
21
|
+
factory({ name: "tokenusages/administration/summary", model: TokenUsageAdminSummary, icon: CoinsIcon });
|
|
21
22
|
|
|
22
23
|
export const TokenUsageAdminTimelineModule = (factory: ModuleFactory) =>
|
|
23
|
-
factory({ name: "tokenusages/administration/timeline", model: TokenUsageAdminTimeline });
|
|
24
|
+
factory({ name: "tokenusages/administration/timeline", model: TokenUsageAdminTimeline, icon: CoinsIcon });
|
|
24
25
|
|
|
25
26
|
export const TokenUsageAdminBreakdownModule = (factory: ModuleFactory) =>
|
|
26
|
-
factory({ name: "tokenusages/administration/breakdown", model: TokenUsageAdminBreakdown });
|
|
27
|
+
factory({ name: "tokenusages/administration/breakdown", model: TokenUsageAdminBreakdown, icon: CoinsIcon });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CoinsIcon } from "lucide-react";
|
|
1
2
|
import { ModuleFactory, ModuleWithPermissions } from "../../permissions";
|
|
2
3
|
import { TokenUsageAdminBreakdown } from "./data/tokenusage-admin-breakdown";
|
|
3
4
|
import { TokenUsageAdminSummary } from "./data/tokenusage-admin-summary";
|
|
@@ -28,11 +29,20 @@ import { TokenUsageReportTimeline } from "./data/tokenusage-report-timeline";
|
|
|
28
29
|
*/
|
|
29
30
|
export const tokenUsageModules = (factory: ModuleFactory) =>
|
|
30
31
|
({
|
|
31
|
-
TokenUsageAdminSummary: factory({
|
|
32
|
-
|
|
32
|
+
TokenUsageAdminSummary: factory({
|
|
33
|
+
name: "tokenusages/administration/summary",
|
|
34
|
+
model: TokenUsageAdminSummary,
|
|
35
|
+
icon: CoinsIcon,
|
|
36
|
+
}),
|
|
37
|
+
TokenUsageAdminTimeline: factory({
|
|
38
|
+
name: "tokenusages/administration/timeline",
|
|
39
|
+
model: TokenUsageAdminTimeline,
|
|
40
|
+
icon: CoinsIcon,
|
|
41
|
+
}),
|
|
33
42
|
TokenUsageAdminBreakdown: factory({
|
|
34
43
|
name: "tokenusages/administration/breakdown",
|
|
35
44
|
model: TokenUsageAdminBreakdown,
|
|
45
|
+
icon: CoinsIcon,
|
|
36
46
|
}),
|
|
37
47
|
TokenUsageReportSummary: factory({ name: "tokenusages/reports/summary", model: TokenUsageReportSummary }),
|
|
38
48
|
TokenUsageReportTimeline: factory({ name: "tokenusages/reports/timeline", model: TokenUsageReportTimeline }),
|
|
@@ -225,6 +225,31 @@ export const CurrentUserProvider = ({ children }: { children: React.ReactNode })
|
|
|
225
225
|
// Track refresh in progress to prevent duplicate API calls
|
|
226
226
|
const isRefreshingRef = useRef(false);
|
|
227
227
|
|
|
228
|
+
// Revalidate the cached user each time the live connection comes up.
|
|
229
|
+
//
|
|
230
|
+
// The cache above is trusted on mount, and changes reach an open page through
|
|
231
|
+
// `company:subscription_updated`. A change made while no page was connected
|
|
232
|
+
// (the browser closed when a Stripe webhook ended the subscription) never
|
|
233
|
+
// arrives, so the cache kept the old company until a token refresh: the sidebar
|
|
234
|
+
// hid "Upgrade" for a company that no longer had a plan, across reloads. The
|
|
235
|
+
// moment the connection comes up (page load, or back from a network drop) is
|
|
236
|
+
// exactly when such an event may have been missed, so re-read the user once.
|
|
237
|
+
const wasConnectedRef = useRef(false);
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (!isConnected) {
|
|
240
|
+
wasConnectedRef.current = false;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (wasConnectedRef.current) return;
|
|
244
|
+
wasConnectedRef.current = true;
|
|
245
|
+
|
|
246
|
+
if (!getCookie("token") || isRefreshingRef.current) return;
|
|
247
|
+
isRefreshingRef.current = true;
|
|
248
|
+
refreshUserRef.current({ skipCookieUpdate: true }).finally(() => {
|
|
249
|
+
isRefreshingRef.current = false;
|
|
250
|
+
});
|
|
251
|
+
}, [isConnected]);
|
|
252
|
+
|
|
228
253
|
// Listen for company WebSocket events that trigger user refresh
|
|
229
254
|
useEffect(() => {
|
|
230
255
|
if (!socket || !isConnected || !currentUser?.company?.id) {
|
|
@@ -7,6 +7,7 @@ import { JsonApiHydratedDataInterface, Modules, rehydrate } from "../../../core"
|
|
|
7
7
|
import { usePageUrlGenerator } from "../../../hooks";
|
|
8
8
|
import { BreadcrumbItemData } from "../../../interfaces";
|
|
9
9
|
import { Action } from "../../../permissions";
|
|
10
|
+
import { getRoleId } from "../../../roles";
|
|
10
11
|
import { UserDeleter, UserEditor, UserReactivator, UserResentInvitationEmail } from "../components/forms";
|
|
11
12
|
import { UserInterface } from "../data";
|
|
12
13
|
import { useCurrentUserContext } from "./CurrentUserContext";
|
|
@@ -18,6 +19,14 @@ interface UserContextType {
|
|
|
18
19
|
|
|
19
20
|
const UserContext = createContext<UserContextType | undefined>(undefined);
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The platform-wide user list administrators use (PlatformUsersContainer). The
|
|
24
|
+
* User module's own `pageUrl` (/users) is the company-scoped list, which a
|
|
25
|
+
* system administrator, who belongs to no company, sees empty. Same path
|
|
26
|
+
* convention as the hrefs in AdminIndexContainer.
|
|
27
|
+
*/
|
|
28
|
+
const USERS_ADMIN_PAGE_URL = "/administration/users";
|
|
29
|
+
|
|
21
30
|
type UserProviderProps = {
|
|
22
31
|
children: ReactNode;
|
|
23
32
|
dehydratedUser?: JsonApiHydratedDataInterface;
|
|
@@ -25,7 +34,7 @@ type UserProviderProps = {
|
|
|
25
34
|
|
|
26
35
|
export const UserProvider = ({ children, dehydratedUser }: UserProviderProps) => {
|
|
27
36
|
const generateUrl = usePageUrlGenerator();
|
|
28
|
-
const { hasPermissionToModule } = useCurrentUserContext<UserInterface>();
|
|
37
|
+
const { hasPermissionToModule, hasRole } = useCurrentUserContext<UserInterface>();
|
|
29
38
|
const t = useTranslations();
|
|
30
39
|
|
|
31
40
|
const [user, setUser] = useState<UserInterface | undefined>(
|
|
@@ -35,10 +44,16 @@ export const UserProvider = ({ children, dehydratedUser }: UserProviderProps) =>
|
|
|
35
44
|
const breadcrumb = () => {
|
|
36
45
|
const response: BreadcrumbItemData[] = [];
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
// Administrators are checked by role, as CompanyProvider does: an app may
|
|
48
|
+
// grant them no module permission, and their list is the administration one.
|
|
49
|
+
const isAdministrator = hasRole(getRoleId().Administrator);
|
|
50
|
+
|
|
51
|
+
// The list root comes first, linked ONLY when it is not the current page
|
|
52
|
+
// (same rule as CompanyProvider).
|
|
53
|
+
if (isAdministrator || hasPermissionToModule({ module: Modules.User, action: Action.Update })) {
|
|
39
54
|
response.push({
|
|
40
55
|
name: t(`entities.users`, { count: 2 }),
|
|
41
|
-
href: generateUrl({ page: Modules.User }),
|
|
56
|
+
...(user ? { href: generateUrl({ page: isAdministrator ? USERS_ADMIN_PAGE_URL : Modules.User }) } : {}),
|
|
42
57
|
});
|
|
43
58
|
}
|
|
44
59
|
|
|
@@ -21,6 +21,8 @@ const permissionsMock = vi.hoisted(() => ({
|
|
|
21
21
|
const socketMock = vi.hoisted(() => {
|
|
22
22
|
const handlers: Record<string, ((data: any) => void)[]> = {};
|
|
23
23
|
return {
|
|
24
|
+
// Read on every render, so a test can drop and restore the live connection
|
|
25
|
+
isConnected: false,
|
|
24
26
|
socket: {
|
|
25
27
|
on: (event: string, handler: (data: any) => void) => {
|
|
26
28
|
(handlers[event] ??= []).push(handler);
|
|
@@ -39,7 +41,7 @@ const socketMock = vi.hoisted(() => {
|
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
vi.mock("../../../../contexts/SocketContext", () => ({
|
|
42
|
-
useSocketContext: () => ({ socket: socketMock.socket, isConnected:
|
|
44
|
+
useSocketContext: () => ({ socket: socketMock.socket, isConnected: socketMock.isConnected }),
|
|
43
45
|
}));
|
|
44
46
|
|
|
45
47
|
vi.mock("../../../../core", () => {
|
|
@@ -113,6 +115,8 @@ describe("CurrentUserProvider", () => {
|
|
|
113
115
|
beforeEach(() => {
|
|
114
116
|
localStorage.clear();
|
|
115
117
|
vi.clearAllMocks();
|
|
118
|
+
socketMock.reset();
|
|
119
|
+
socketMock.isConnected = false;
|
|
116
120
|
});
|
|
117
121
|
|
|
118
122
|
it("keeps user hydrated from localStorage when token cookie is absent", async () => {
|
|
@@ -161,7 +165,7 @@ describe("CurrentUserProvider", () => {
|
|
|
161
165
|
expect(updateToken).not.toHaveBeenCalled();
|
|
162
166
|
});
|
|
163
167
|
|
|
164
|
-
it("does not hit the API when localStorage already holds the user", async () => {
|
|
168
|
+
it("does not hit the API on mount when localStorage already holds the user and no live connection is up", async () => {
|
|
165
169
|
vi.mocked(getCookie).mockImplementation((name: any) => (name === "token" ? "jwt" : undefined));
|
|
166
170
|
localStorage.setItem("user", JSON.stringify({ id: "cached", roles: [], company: null, modules: [] }));
|
|
167
171
|
|
|
@@ -239,6 +243,117 @@ describe("CurrentUserProvider", () => {
|
|
|
239
243
|
});
|
|
240
244
|
});
|
|
241
245
|
|
|
246
|
+
describe("CurrentUserProvider live connection revalidation", () => {
|
|
247
|
+
const ACTIVE = { id: "user-1", roles: [], modules: [], company: { id: "company-1", isActiveSubscription: true } };
|
|
248
|
+
const ENDED = { id: "user-1", roles: [], modules: [], company: { id: "company-1", isActiveSubscription: false } };
|
|
249
|
+
|
|
250
|
+
function SubscriptionConsumer() {
|
|
251
|
+
const { company } = useCurrentUserContext();
|
|
252
|
+
return <div data-testid="active">{company ? String((company as any).isActiveSubscription) : "null"}</div>;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
beforeEach(() => {
|
|
256
|
+
localStorage.clear();
|
|
257
|
+
socketMock.reset();
|
|
258
|
+
vi.clearAllMocks();
|
|
259
|
+
vi.mocked(getCookie).mockImplementation((name: any) => (name === "token" ? "jwt" : undefined));
|
|
260
|
+
vi.mocked(UserService.findFullUser).mockResolvedValue({ ...ENDED, dehydrate: () => ENDED } as any);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("re-reads the user when the live connection comes up, so a change missed while no page was open shows", async () => {
|
|
264
|
+
// The subscription ended (Stripe webhook) while the browser was closed: the cache still says active
|
|
265
|
+
localStorage.setItem("user", JSON.stringify(ACTIVE));
|
|
266
|
+
socketMock.isConnected = true;
|
|
267
|
+
|
|
268
|
+
const { getByTestId } = render(
|
|
269
|
+
<CurrentUserProvider>
|
|
270
|
+
<SubscriptionConsumer />
|
|
271
|
+
</CurrentUserProvider>,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
await act(async () => {
|
|
275
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(UserService.findFullUser).toHaveBeenCalledTimes(1);
|
|
279
|
+
expect(getByTestId("active").textContent).toBe("false");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("re-reads once per connection, not on every render", async () => {
|
|
283
|
+
localStorage.setItem("user", JSON.stringify(ACTIVE));
|
|
284
|
+
socketMock.isConnected = true;
|
|
285
|
+
|
|
286
|
+
const { rerender } = render(
|
|
287
|
+
<CurrentUserProvider>
|
|
288
|
+
<SubscriptionConsumer />
|
|
289
|
+
</CurrentUserProvider>,
|
|
290
|
+
);
|
|
291
|
+
await act(async () => {
|
|
292
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
293
|
+
});
|
|
294
|
+
rerender(
|
|
295
|
+
<CurrentUserProvider>
|
|
296
|
+
<SubscriptionConsumer />
|
|
297
|
+
</CurrentUserProvider>,
|
|
298
|
+
);
|
|
299
|
+
await act(async () => {
|
|
300
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(UserService.findFullUser).toHaveBeenCalledTimes(1);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("re-reads again after the connection drops and comes back", async () => {
|
|
307
|
+
localStorage.setItem("user", JSON.stringify(ACTIVE));
|
|
308
|
+
socketMock.isConnected = true;
|
|
309
|
+
|
|
310
|
+
const tree = (
|
|
311
|
+
<CurrentUserProvider>
|
|
312
|
+
<SubscriptionConsumer />
|
|
313
|
+
</CurrentUserProvider>
|
|
314
|
+
);
|
|
315
|
+
const { rerender } = render(tree);
|
|
316
|
+
await act(async () => {
|
|
317
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
socketMock.isConnected = false;
|
|
321
|
+
rerender(
|
|
322
|
+
<CurrentUserProvider>
|
|
323
|
+
<SubscriptionConsumer />
|
|
324
|
+
</CurrentUserProvider>,
|
|
325
|
+
);
|
|
326
|
+
socketMock.isConnected = true;
|
|
327
|
+
rerender(
|
|
328
|
+
<CurrentUserProvider>
|
|
329
|
+
<SubscriptionConsumer />
|
|
330
|
+
</CurrentUserProvider>,
|
|
331
|
+
);
|
|
332
|
+
await act(async () => {
|
|
333
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
expect(UserService.findFullUser).toHaveBeenCalledTimes(2);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("does not re-read without a session cookie", async () => {
|
|
340
|
+
vi.mocked(getCookie).mockReturnValue(undefined);
|
|
341
|
+
localStorage.setItem("user", JSON.stringify(ACTIVE));
|
|
342
|
+
socketMock.isConnected = true;
|
|
343
|
+
|
|
344
|
+
render(
|
|
345
|
+
<CurrentUserProvider>
|
|
346
|
+
<SubscriptionConsumer />
|
|
347
|
+
</CurrentUserProvider>,
|
|
348
|
+
);
|
|
349
|
+
await act(async () => {
|
|
350
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
expect(UserService.findFullUser).not.toHaveBeenCalled();
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
242
357
|
describe("CurrentUserProvider token updates", () => {
|
|
243
358
|
const STORED_USER = {
|
|
244
359
|
id: "user-1",
|
|
@@ -265,6 +380,9 @@ describe("CurrentUserProvider token updates", () => {
|
|
|
265
380
|
localStorage.clear();
|
|
266
381
|
socketMock.reset();
|
|
267
382
|
vi.clearAllMocks();
|
|
383
|
+
// Connected, with no session cookie: these tests are about socket events, not the reconnect re-read
|
|
384
|
+
socketMock.isConnected = true;
|
|
385
|
+
vi.mocked(getCookie).mockReturnValue(undefined);
|
|
268
386
|
});
|
|
269
387
|
|
|
270
388
|
it("patches balances from company:credits_updated without refetching the user", async () => {
|
|
@@ -352,6 +470,7 @@ describe("CurrentUserProvider path permissions", () => {
|
|
|
352
470
|
beforeEach(() => {
|
|
353
471
|
localStorage.clear();
|
|
354
472
|
vi.clearAllMocks();
|
|
473
|
+
socketMock.isConnected = false;
|
|
355
474
|
ctx = undefined;
|
|
356
475
|
permissionsMock.checkPermissions.mockImplementation(() => true);
|
|
357
476
|
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { useSharedContext } from "../../../../contexts/SharedContext";
|
|
4
|
+
import { UserProvider } from "../UserContext";
|
|
5
|
+
|
|
6
|
+
const access = vi.hoisted(() => ({ isAdministrator: false, canUpdateUsers: false }));
|
|
7
|
+
|
|
8
|
+
vi.mock("../CurrentUserContext", () => ({
|
|
9
|
+
useCurrentUserContext: () => ({
|
|
10
|
+
hasRole: (roleId: string) => access.isAdministrator && roleId === "administrator",
|
|
11
|
+
hasPermissionToModule: () => access.canUpdateUsers,
|
|
12
|
+
}),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("../../../../roles", () => ({
|
|
16
|
+
getRoleId: () => ({ Administrator: "administrator" }),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// The title functions are not under test; the real forms pull in the editor stack.
|
|
20
|
+
vi.mock("../../components/forms", () => ({
|
|
21
|
+
UserDeleter: () => null,
|
|
22
|
+
UserEditor: () => null,
|
|
23
|
+
UserReactivator: () => null,
|
|
24
|
+
UserResentInvitationEmail: () => null,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// The registry throws for modules nobody registered. The dehydrated user is
|
|
28
|
+
// handed back as-is so the spec does not depend on the User model.
|
|
29
|
+
vi.mock("../../../../core", async (importOriginal) => {
|
|
30
|
+
const actual = await importOriginal<typeof import("../../../../core")>();
|
|
31
|
+
return {
|
|
32
|
+
...actual,
|
|
33
|
+
Modules: { User: { name: "users", pageUrl: "/users" } },
|
|
34
|
+
rehydrate: (_module: unknown, data: unknown) => data,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// next-intl is globally mocked in vitest.setup.ts with `t = (key) => key`, so a
|
|
39
|
+
// translated label renders as its own key.
|
|
40
|
+
function BreadcrumbProbe() {
|
|
41
|
+
const { breadcrumbs } = useSharedContext();
|
|
42
|
+
return <span data-testid="breadcrumbs">{JSON.stringify(breadcrumbs)}</span>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const user = { id: "u1", name: "Ada", isDeleted: false, isActivated: true } as any;
|
|
46
|
+
|
|
47
|
+
const renderBreadcrumbs = (dehydratedUser?: any) => {
|
|
48
|
+
render(
|
|
49
|
+
<UserProvider dehydratedUser={dehydratedUser}>
|
|
50
|
+
<BreadcrumbProbe />
|
|
51
|
+
</UserProvider>,
|
|
52
|
+
);
|
|
53
|
+
return JSON.parse(screen.getByTestId("breadcrumbs").textContent ?? "[]");
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
describe("UserContext breadcrumb", () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
access.isAdministrator = false;
|
|
59
|
+
access.canUpdateUsers = false;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("shows an administrator the users list as an unlinked root, even without the module permission", () => {
|
|
63
|
+
access.isAdministrator = true;
|
|
64
|
+
|
|
65
|
+
expect(renderBreadcrumbs()).toEqual([{ name: "entities.users" }]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("links an administrator's user detail back to the administration users list", () => {
|
|
69
|
+
access.isAdministrator = true;
|
|
70
|
+
|
|
71
|
+
expect(renderBreadcrumbs(user)).toEqual([
|
|
72
|
+
{ name: "entities.users", href: "/administration/users" },
|
|
73
|
+
{ name: "Ada", href: "/users/u1" },
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("shows a user manager the company users list as an unlinked root", () => {
|
|
78
|
+
access.canUpdateUsers = true;
|
|
79
|
+
|
|
80
|
+
expect(renderBreadcrumbs()).toEqual([{ name: "entities.users" }]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("links a user manager's user detail back to the company users list", () => {
|
|
84
|
+
access.canUpdateUsers = true;
|
|
85
|
+
|
|
86
|
+
expect(renderBreadcrumbs(user)).toEqual([
|
|
87
|
+
{ name: "entities.users", href: "/users" },
|
|
88
|
+
{ name: "Ada", href: "/users/u1" },
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("shows only the user to a viewer who can neither manage users nor administer", () => {
|
|
93
|
+
expect(renderBreadcrumbs(user)).toEqual([{ name: "Ada", href: "/users/u1" }]);
|
|
94
|
+
});
|
|
95
|
+
});
|