@carlonicora/nextjs-jsonapi 5.3.0 → 5.5.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-WTSQANKI.mjs → BlockNoteEditor-GVQCKRMU.mjs} +9 -8
- package/dist/BlockNoteEditor-GVQCKRMU.mjs.map +1 -0
- package/dist/{BlockNoteEditor-UJ6363LC.js → BlockNoteEditor-THBJH2UR.js} +24 -23
- package/dist/BlockNoteEditor-THBJH2UR.js.map +1 -0
- package/dist/{BlockNoteViewer-JVKWJZ6J.mjs → BlockNoteViewer-U6E3DS4F.mjs} +3 -3
- package/dist/{BlockNoteViewer-EL4JFB6P.js → BlockNoteViewer-VZOVKTFO.js} +4 -4
- package/dist/{BlockNoteViewer-EL4JFB6P.js.map → BlockNoteViewer-VZOVKTFO.js.map} +1 -1
- package/dist/BlogCategoryFields-DO48vOTE.d.mts +14 -0
- package/dist/BlogCategoryFields-DO48vOTE.d.ts +14 -0
- package/dist/BlogPostInterface-BB_4wcDR.d.mts +124 -0
- package/dist/BlogPostInterface-bWAT1rfR.d.ts +124 -0
- package/dist/billing/index.js +442 -442
- package/dist/billing/index.mjs +4 -4
- package/dist/blog-public.types-BbrzsC1c.d.mts +30 -0
- package/dist/blog-public.types-BbrzsC1c.d.ts +30 -0
- package/dist/chunk-3CGROPF3.mjs +107 -0
- package/dist/chunk-3CGROPF3.mjs.map +1 -0
- package/dist/{chunk-QKZUE5OF.mjs → chunk-3YW35YHQ.mjs} +1 -101
- package/dist/chunk-3YW35YHQ.mjs.map +1 -0
- package/dist/{chunk-6WU74VFU.mjs → chunk-6SQYXUVP.mjs} +2 -2
- package/dist/{chunk-CRI4T47Y.js → chunk-G6UE6MEP.js} +814 -59
- package/dist/chunk-G6UE6MEP.js.map +1 -0
- package/dist/{chunk-XGTQWBDJ.js → chunk-KSYQEKJX.js} +11 -111
- package/dist/chunk-KSYQEKJX.js.map +1 -0
- package/dist/{chunk-EO75CV76.js → chunk-S4YJ5WCW.js} +8 -8
- package/dist/{chunk-EO75CV76.js.map → chunk-S4YJ5WCW.js.map} +1 -1
- package/dist/{chunk-3BAM23VA.js → chunk-TY5FYP3G.js} +930 -883
- package/dist/chunk-TY5FYP3G.js.map +1 -0
- package/dist/{chunk-JQWI62NV.mjs → chunk-VC5A3AUZ.mjs} +776 -21
- package/dist/chunk-VC5A3AUZ.mjs.map +1 -0
- package/dist/{chunk-FNTQQFLH.mjs → chunk-W7F5BDSV.mjs} +2 -2
- package/dist/{chunk-QQQE3N2X.js → chunk-WPYT4UKJ.js} +3 -3
- package/dist/{chunk-QQQE3N2X.js.map → chunk-WPYT4UKJ.js.map} +1 -1
- package/dist/{chunk-MIZTWBPX.mjs → chunk-X5WIUUNG.mjs} +1485 -1438
- package/dist/chunk-X5WIUUNG.mjs.map +1 -0
- package/dist/chunk-YDYVJM4I.js +107 -0
- package/dist/chunk-YDYVJM4I.js.map +1 -0
- 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 +46 -12
- package/dist/components/index.d.ts +46 -12
- 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 +214 -3
- package/dist/core/index.d.ts +214 -3
- package/dist/core/index.js +38 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +37 -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 +9 -8
- package/dist/features/analytics/server.js.map +1 -1
- package/dist/features/analytics/server.mjs +7 -6
- package/dist/features/analytics/server.mjs.map +1 -1
- package/dist/features/blog/index.css +30 -0
- package/dist/features/blog/index.css.map +1 -0
- package/dist/features/blog/index.d.mts +276 -0
- package/dist/features/blog/index.d.ts +276 -0
- package/dist/features/blog/index.js +1810 -0
- package/dist/features/blog/index.js.map +1 -0
- package/dist/features/blog/index.mjs +1810 -0
- package/dist/features/blog/index.mjs.map +1 -0
- package/dist/features/blog/server.d.mts +36 -0
- package/dist/features/blog/server.d.ts +36 -0
- package/dist/features/blog/server.js +167 -0
- package/dist/features/blog/server.js.map +1 -0
- package/dist/features/blog/server.mjs +167 -0
- package/dist/features/blog/server.mjs.map +1 -0
- 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/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +39 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +38 -4
- package/dist/{s3.service-CY-jgOAM.d.ts → s3.service-Bz_K-tjz.d.ts} +11 -0
- package/dist/{s3.service-Sri0QP7Z.d.mts → s3.service-D5zNCEW3.d.mts} +11 -0
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +9 -7
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +13 -11
- package/messages/en.json +93 -0
- package/messages/it.json +93 -0
- package/package.json +19 -1
- package/src/components/editors/BlockNoteEditor.tsx +13 -3
- 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/forms/FormBlockNote.tsx +4 -0
- package/src/components/index.ts +1 -0
- package/src/components/navigations/ContentTitle.tsx +14 -3
- package/src/components/viewers/BlockNoteStaticViewer.tsx +44 -0
- package/src/core/index.ts +4 -0
- package/src/core/registry/ModuleRegistry.ts +6 -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/stripe-price/stripe-price.module.ts +2 -0
- package/src/features/billing/stripe-product/stripe-product.module.ts +2 -0
- package/src/features/blog/BlogModules.ts +49 -0
- package/src/features/blog/components/admin/BlogAdminContainer.tsx +39 -0
- package/src/features/blog/components/admin/BlogCategoriesList.tsx +56 -0
- package/src/features/blog/components/admin/BlogCategoryEditor.tsx +230 -0
- package/src/features/blog/components/admin/BlogCoverImageField.tsx +123 -0
- package/src/features/blog/components/admin/BlogLayoutSetting.tsx +86 -0
- package/src/features/blog/components/admin/BlogPostComparison.tsx +54 -0
- package/src/features/blog/components/admin/BlogPostContainer.tsx +101 -0
- package/src/features/blog/components/admin/BlogPostDeleter.tsx +35 -0
- package/src/features/blog/components/admin/BlogPostEditor.tsx +216 -0
- package/src/features/blog/components/admin/BlogPostPublishButton.tsx +50 -0
- package/src/features/blog/components/admin/BlogPostTranslationEditor.tsx +118 -0
- package/src/features/blog/components/admin/BlogPostTranslationPanel.tsx +136 -0
- package/src/features/blog/components/admin/BlogPostsList.tsx +58 -0
- package/src/features/blog/components/admin/BlogTranslationStatus.tsx +104 -0
- package/src/features/blog/components/admin/__tests__/BlogPostTranslationPanel.spec.tsx +100 -0
- package/src/features/blog/components/admin/__tests__/BlogPostsList.spec.tsx +30 -0
- package/src/features/blog/components/admin/index.ts +14 -0
- package/src/features/blog/components/public/BlogCategoryChips.tsx +59 -0
- package/src/features/blog/components/public/BlogList.tsx +21 -0
- package/src/features/blog/components/public/BlogListColumn.tsx +53 -0
- package/src/features/blog/components/public/BlogListFeatured.tsx +57 -0
- package/src/features/blog/components/public/BlogListParts.tsx +74 -0
- package/src/features/blog/components/public/BlogListRail.tsx +62 -0
- package/src/features/blog/components/public/BlogPostCard.tsx +125 -0
- package/src/features/blog/components/public/BlogPostMeta.tsx +49 -0
- package/src/features/blog/components/public/BlogPostView.tsx +80 -0
- package/src/features/blog/components/public/BlogSearchBox.tsx +52 -0
- package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +175 -0
- package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +134 -0
- package/src/features/blog/components/public/blogPublicHelpers.ts +30 -0
- package/src/features/blog/components/public/index.ts +3 -0
- package/src/features/blog/contexts/BlogPostContext.tsx +66 -0
- package/src/features/blog/contexts/index.ts +1 -0
- package/src/features/blog/data/BlogCategory.ts +93 -0
- package/src/features/blog/data/BlogCategoryFields.ts +4 -0
- package/src/features/blog/data/BlogCategoryInterface.ts +24 -0
- package/src/features/blog/data/BlogCategoryService.ts +54 -0
- package/src/features/blog/data/BlogCategoryTranslation.ts +90 -0
- package/src/features/blog/data/BlogCategoryTranslationInterface.ts +22 -0
- package/src/features/blog/data/BlogCategoryTranslationService.ts +30 -0
- package/src/features/blog/data/BlogPost.spec.ts +123 -0
- package/src/features/blog/data/BlogPost.ts +159 -0
- package/src/features/blog/data/BlogPostFields.ts +7 -0
- package/src/features/blog/data/BlogPostInterface.ts +39 -0
- package/src/features/blog/data/BlogPostService.ts +82 -0
- package/src/features/blog/data/BlogPostTranslation.spec.ts +73 -0
- package/src/features/blog/data/BlogPostTranslation.ts +110 -0
- package/src/features/blog/data/BlogPostTranslationInterface.ts +27 -0
- package/src/features/blog/data/BlogPostTranslationService.ts +30 -0
- package/src/features/blog/data/BlogSettings.ts +37 -0
- package/src/features/blog/data/BlogSettingsInterface.ts +11 -0
- package/src/features/blog/data/BlogSettingsService.ts +24 -0
- package/src/features/blog/data/index.ts +17 -0
- package/src/features/blog/hooks/index.ts +3 -0
- package/src/features/blog/hooks/useBlogCategoryNames.ts +34 -0
- package/src/features/blog/hooks/useBlogCategoryTableStructure.tsx +84 -0
- package/src/features/blog/hooks/useBlogPostTableStructure.tsx +126 -0
- package/src/features/blog/index.ts +15 -0
- package/src/features/blog/server/fetchers.ts +169 -0
- package/src/features/blog/server/toPublic.spec.ts +91 -0
- package/src/features/blog/server/toPublic.ts +36 -0
- package/src/features/blog/server-entry.ts +17 -0
- package/src/features/blog/types/blog-public.types.ts +34 -0
- package/src/features/blog/types/blog.constants.ts +40 -0
- 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/s3/data/s3.service.ts +17 -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/UserContext.tsx +18 -3
- package/src/features/user/contexts/__tests__/UserContext.spec.tsx +95 -0
- package/dist/BlockNoteEditor-UJ6363LC.js.map +0 -1
- package/dist/BlockNoteEditor-WTSQANKI.mjs.map +0 -1
- package/dist/chunk-3BAM23VA.js.map +0 -1
- package/dist/chunk-CRI4T47Y.js.map +0 -1
- package/dist/chunk-JQWI62NV.mjs.map +0 -1
- package/dist/chunk-MIZTWBPX.mjs.map +0 -1
- package/dist/chunk-QKZUE5OF.mjs.map +0 -1
- package/dist/chunk-XGTQWBDJ.js.map +0 -1
- /package/dist/{BlockNoteViewer-JVKWJZ6J.mjs.map → BlockNoteViewer-U6E3DS4F.mjs.map} +0 -0
- /package/dist/{chunk-6WU74VFU.mjs.map → chunk-6SQYXUVP.mjs.map} +0 -0
- /package/dist/{chunk-FNTQQFLH.mjs.map → chunk-W7F5BDSV.mjs.map} +0 -0
|
@@ -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
|
}
|
|
@@ -4,7 +4,6 @@ import { useTranslations } from "next-intl";
|
|
|
4
4
|
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
5
5
|
import { Modules, NextRef } from "../../../core";
|
|
6
6
|
import { SharedProvider } from "../../../contexts";
|
|
7
|
-
import { usePageUrlGenerator } from "../../../hooks";
|
|
8
7
|
import { BreadcrumbItemData } from "../../../interfaces";
|
|
9
8
|
import { AnalyticsAdminFilterBar } from "../components/AnalyticsAdminFilterBar";
|
|
10
9
|
import type { AnalyticsBreakdownInterface } from "../data/analytics-breakdown.interface";
|
|
@@ -19,13 +18,6 @@ import {
|
|
|
19
18
|
} from "../data/analytics.types";
|
|
20
19
|
import { AnalyticsAdminService } from "../data/AnalyticsAdminService";
|
|
21
20
|
|
|
22
|
-
/**
|
|
23
|
-
* Default route the breadcrumb links back to. A host app that mounts the page
|
|
24
|
-
* elsewhere overrides it with the `pageUrl` prop — the constant stays here
|
|
25
|
-
* because the breadcrumb is built by this provider, not by the app.
|
|
26
|
-
*/
|
|
27
|
-
const ANALYTICS_ADMIN_PAGE_URL = "/administration/analytics";
|
|
28
|
-
|
|
29
21
|
/** Default base route of the journey page each session row links to. */
|
|
30
22
|
const ANALYTICS_SESSION_PAGE_URL = "/administration/analytics/sessions";
|
|
31
23
|
|
|
@@ -86,7 +78,11 @@ type AnalyticsAdminProviderProps = {
|
|
|
86
78
|
initialFrom?: string;
|
|
87
79
|
/** ISO 8601 instant. Defaults to now. */
|
|
88
80
|
initialTo?: string;
|
|
89
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* @deprecated No longer read. This provider only serves the dashboard, which
|
|
83
|
+
* is the current page, so its breadcrumb is not linked. Kept so existing
|
|
84
|
+
* callers still compile.
|
|
85
|
+
*/
|
|
90
86
|
pageUrl?: string;
|
|
91
87
|
/** Base route of the journey page each session row links to. */
|
|
92
88
|
sessionPageUrl?: string;
|
|
@@ -105,11 +101,9 @@ export const AnalyticsAdminProvider = ({
|
|
|
105
101
|
children,
|
|
106
102
|
initialFrom,
|
|
107
103
|
initialTo,
|
|
108
|
-
pageUrl = ANALYTICS_ADMIN_PAGE_URL,
|
|
109
104
|
sessionPageUrl = ANALYTICS_SESSION_PAGE_URL,
|
|
110
105
|
}: AnalyticsAdminProviderProps) => {
|
|
111
106
|
const t = useTranslations();
|
|
112
|
-
const generateUrl = usePageUrlGenerator();
|
|
113
107
|
|
|
114
108
|
const [filters, setFilterState] = useState<AnalyticsAdminFilterState>(() => {
|
|
115
109
|
const range = defaultRange();
|
|
@@ -205,12 +199,9 @@ export const AnalyticsAdminProvider = ({
|
|
|
205
199
|
}
|
|
206
200
|
}, [sessionsNextPage]);
|
|
207
201
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
href: generateUrl({ page: pageUrl }),
|
|
212
|
-
},
|
|
213
|
-
];
|
|
202
|
+
// Unlinked: this provider only serves the dashboard, so the root is always
|
|
203
|
+
// the current page. The journey page publishes its own linked breadcrumb.
|
|
204
|
+
const breadcrumb = (): BreadcrumbItemData[] => [{ name: t("analytics.admin.title") }];
|
|
214
205
|
|
|
215
206
|
const title = () => ({
|
|
216
207
|
type: t("analytics.admin.title"),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { useSharedContext } from "../../../../contexts/SharedContext";
|
|
3
4
|
import { AnalyticsAdminService } from "../../data/AnalyticsAdminService";
|
|
4
5
|
import { AnalyticsAdminProvider, useAnalyticsAdmin } from "../AnalyticsAdminContext";
|
|
5
6
|
|
|
@@ -40,6 +41,11 @@ function Probe() {
|
|
|
40
41
|
);
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function BreadcrumbProbe() {
|
|
45
|
+
const { breadcrumbs } = useSharedContext();
|
|
46
|
+
return <span data-testid="breadcrumbs">{JSON.stringify(breadcrumbs)}</span>;
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
describe("AnalyticsAdminContext", () => {
|
|
44
50
|
// The mocked service is module-level, so its call counts survive across tests.
|
|
45
51
|
beforeEach(() => {
|
|
@@ -137,4 +143,15 @@ describe("AnalyticsAdminContext", () => {
|
|
|
137
143
|
);
|
|
138
144
|
expect(screen.getByTestId("has-more")).toHaveTextContent("false");
|
|
139
145
|
});
|
|
146
|
+
|
|
147
|
+
it("publishes the dashboard as an unlinked breadcrumb, since it is the current page", async () => {
|
|
148
|
+
render(
|
|
149
|
+
<AnalyticsAdminProvider>
|
|
150
|
+
<BreadcrumbProbe />
|
|
151
|
+
</AnalyticsAdminProvider>,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
await waitFor(() => expect(AnalyticsAdminService.getSessions).toHaveBeenCalled());
|
|
155
|
+
expect(screen.getByTestId("breadcrumbs")).toHaveTextContent(JSON.stringify([{ name: "analytics.admin.title" }]));
|
|
156
|
+
});
|
|
140
157
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CreditCardIcon } from "lucide-react";
|
|
1
2
|
import { ModuleFactory } from "../../../permissions";
|
|
2
3
|
import { StripePrice } from "./data/stripe-price";
|
|
3
4
|
|
|
@@ -11,4 +12,5 @@ export const StripePriceModule = (factory: ModuleFactory) =>
|
|
|
11
12
|
pageUrl: "/administration/prices",
|
|
12
13
|
model: StripePrice,
|
|
13
14
|
moduleId: "a7d3e5f1-8c9b-4a2e-b6d7-3f1c8e9a4b5d",
|
|
15
|
+
icon: CreditCardIcon,
|
|
14
16
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CreditCardIcon } from "lucide-react";
|
|
1
2
|
import { ModuleFactory } from "../../../permissions";
|
|
2
3
|
import { StripeProduct } from "./data";
|
|
3
4
|
|
|
@@ -11,4 +12,5 @@ export const StripeProductModule = (factory: ModuleFactory) =>
|
|
|
11
12
|
pageUrl: "/administration/products",
|
|
12
13
|
model: StripeProduct,
|
|
13
14
|
moduleId: "f8c4a1e9-3b2d-4f7a-9e5c-1d8a6b3c9f2e",
|
|
15
|
+
icon: CreditCardIcon,
|
|
14
16
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NewspaperIcon } from "lucide-react";
|
|
2
|
+
import { ModuleFactory } from "../../permissions";
|
|
3
|
+
import { BlogCategory } from "./data/BlogCategory";
|
|
4
|
+
import { BlogCategoryTranslation } from "./data/BlogCategoryTranslation";
|
|
5
|
+
import { BlogPost } from "./data/BlogPost";
|
|
6
|
+
import { BlogPostTranslation } from "./data/BlogPostTranslation";
|
|
7
|
+
import { BlogSettings } from "./data/BlogSettings";
|
|
8
|
+
import { BLOG_MODULE_ID } from "./types/blog.constants";
|
|
9
|
+
|
|
10
|
+
export const BlogPostModule = (factory: ModuleFactory) =>
|
|
11
|
+
factory({
|
|
12
|
+
moduleId: BLOG_MODULE_ID,
|
|
13
|
+
pageUrl: "/administration/blog",
|
|
14
|
+
name: "blog-posts",
|
|
15
|
+
model: BlogPost,
|
|
16
|
+
icon: NewspaperIcon,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const BlogPostTranslationModule = (factory: ModuleFactory) =>
|
|
20
|
+
factory({
|
|
21
|
+
moduleId: BLOG_MODULE_ID,
|
|
22
|
+
name: "blog-post-translations",
|
|
23
|
+
model: BlogPostTranslation,
|
|
24
|
+
icon: NewspaperIcon,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const BlogCategoryModule = (factory: ModuleFactory) =>
|
|
28
|
+
factory({
|
|
29
|
+
moduleId: BLOG_MODULE_ID,
|
|
30
|
+
name: "blog-categories",
|
|
31
|
+
model: BlogCategory,
|
|
32
|
+
icon: NewspaperIcon,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const BlogCategoryTranslationModule = (factory: ModuleFactory) =>
|
|
36
|
+
factory({
|
|
37
|
+
moduleId: BLOG_MODULE_ID,
|
|
38
|
+
name: "blog-category-translations",
|
|
39
|
+
model: BlogCategoryTranslation,
|
|
40
|
+
icon: NewspaperIcon,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const BlogSettingsModule = (factory: ModuleFactory) =>
|
|
44
|
+
factory({
|
|
45
|
+
moduleId: BLOG_MODULE_ID,
|
|
46
|
+
name: "blog-settings",
|
|
47
|
+
model: BlogSettings,
|
|
48
|
+
icon: NewspaperIcon,
|
|
49
|
+
});
|