@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.
Files changed (233) hide show
  1. package/dist/{BlockNoteEditor-WTSQANKI.mjs → BlockNoteEditor-GVQCKRMU.mjs} +9 -8
  2. package/dist/BlockNoteEditor-GVQCKRMU.mjs.map +1 -0
  3. package/dist/{BlockNoteEditor-UJ6363LC.js → BlockNoteEditor-THBJH2UR.js} +24 -23
  4. package/dist/BlockNoteEditor-THBJH2UR.js.map +1 -0
  5. package/dist/{BlockNoteViewer-JVKWJZ6J.mjs → BlockNoteViewer-U6E3DS4F.mjs} +3 -3
  6. package/dist/{BlockNoteViewer-EL4JFB6P.js → BlockNoteViewer-VZOVKTFO.js} +4 -4
  7. package/dist/{BlockNoteViewer-EL4JFB6P.js.map → BlockNoteViewer-VZOVKTFO.js.map} +1 -1
  8. package/dist/BlogCategoryFields-DO48vOTE.d.mts +14 -0
  9. package/dist/BlogCategoryFields-DO48vOTE.d.ts +14 -0
  10. package/dist/BlogPostInterface-BB_4wcDR.d.mts +124 -0
  11. package/dist/BlogPostInterface-bWAT1rfR.d.ts +124 -0
  12. package/dist/billing/index.js +442 -442
  13. package/dist/billing/index.mjs +4 -4
  14. package/dist/blog-public.types-BbrzsC1c.d.mts +30 -0
  15. package/dist/blog-public.types-BbrzsC1c.d.ts +30 -0
  16. package/dist/chunk-3CGROPF3.mjs +107 -0
  17. package/dist/chunk-3CGROPF3.mjs.map +1 -0
  18. package/dist/{chunk-QKZUE5OF.mjs → chunk-3YW35YHQ.mjs} +1 -101
  19. package/dist/chunk-3YW35YHQ.mjs.map +1 -0
  20. package/dist/{chunk-6WU74VFU.mjs → chunk-6SQYXUVP.mjs} +2 -2
  21. package/dist/{chunk-CRI4T47Y.js → chunk-G6UE6MEP.js} +814 -59
  22. package/dist/chunk-G6UE6MEP.js.map +1 -0
  23. package/dist/{chunk-XGTQWBDJ.js → chunk-KSYQEKJX.js} +11 -111
  24. package/dist/chunk-KSYQEKJX.js.map +1 -0
  25. package/dist/{chunk-EO75CV76.js → chunk-S4YJ5WCW.js} +8 -8
  26. package/dist/{chunk-EO75CV76.js.map → chunk-S4YJ5WCW.js.map} +1 -1
  27. package/dist/{chunk-3BAM23VA.js → chunk-TY5FYP3G.js} +930 -883
  28. package/dist/chunk-TY5FYP3G.js.map +1 -0
  29. package/dist/{chunk-JQWI62NV.mjs → chunk-VC5A3AUZ.mjs} +776 -21
  30. package/dist/chunk-VC5A3AUZ.mjs.map +1 -0
  31. package/dist/{chunk-FNTQQFLH.mjs → chunk-W7F5BDSV.mjs} +2 -2
  32. package/dist/{chunk-QQQE3N2X.js → chunk-WPYT4UKJ.js} +3 -3
  33. package/dist/{chunk-QQQE3N2X.js.map → chunk-WPYT4UKJ.js.map} +1 -1
  34. package/dist/{chunk-MIZTWBPX.mjs → chunk-X5WIUUNG.mjs} +1485 -1438
  35. package/dist/chunk-X5WIUUNG.mjs.map +1 -0
  36. package/dist/chunk-YDYVJM4I.js +107 -0
  37. package/dist/chunk-YDYVJM4I.js.map +1 -0
  38. package/dist/client/index.js +5 -5
  39. package/dist/client/index.js.map +1 -1
  40. package/dist/client/index.mjs +4 -4
  41. package/dist/components/editors/server.d.mts +15 -0
  42. package/dist/components/editors/server.d.ts +15 -0
  43. package/dist/components/editors/server.js +38 -0
  44. package/dist/components/editors/server.js.map +1 -0
  45. package/dist/components/editors/server.mjs +38 -0
  46. package/dist/components/editors/server.mjs.map +1 -0
  47. package/dist/components/index.d.mts +46 -12
  48. package/dist/components/index.d.ts +46 -12
  49. package/dist/components/index.js +7 -5
  50. package/dist/components/index.js.map +1 -1
  51. package/dist/components/index.mjs +8 -6
  52. package/dist/contexts/index.d.mts +9 -1
  53. package/dist/contexts/index.d.ts +9 -1
  54. package/dist/contexts/index.js +5 -5
  55. package/dist/contexts/index.mjs +4 -4
  56. package/dist/core/index.d.mts +214 -3
  57. package/dist/core/index.d.ts +214 -3
  58. package/dist/core/index.js +38 -4
  59. package/dist/core/index.js.map +1 -1
  60. package/dist/core/index.mjs +37 -3
  61. package/dist/email-automations/index.d.mts +3 -0
  62. package/dist/email-automations/index.d.ts +3 -0
  63. package/dist/email-automations/index.js +99 -95
  64. package/dist/email-automations/index.js.map +1 -1
  65. package/dist/email-automations/index.mjs +30 -26
  66. package/dist/email-automations/index.mjs.map +1 -1
  67. package/dist/features/analytics/index.d.mts +11 -4
  68. package/dist/features/analytics/index.d.ts +11 -4
  69. package/dist/features/analytics/index.js +127 -135
  70. package/dist/features/analytics/index.js.map +1 -1
  71. package/dist/features/analytics/index.mjs +16 -24
  72. package/dist/features/analytics/index.mjs.map +1 -1
  73. package/dist/features/analytics/server.d.mts +5 -2
  74. package/dist/features/analytics/server.d.ts +5 -2
  75. package/dist/features/analytics/server.js +9 -8
  76. package/dist/features/analytics/server.js.map +1 -1
  77. package/dist/features/analytics/server.mjs +7 -6
  78. package/dist/features/analytics/server.mjs.map +1 -1
  79. package/dist/features/blog/index.css +30 -0
  80. package/dist/features/blog/index.css.map +1 -0
  81. package/dist/features/blog/index.d.mts +276 -0
  82. package/dist/features/blog/index.d.ts +276 -0
  83. package/dist/features/blog/index.js +1810 -0
  84. package/dist/features/blog/index.js.map +1 -0
  85. package/dist/features/blog/index.mjs +1810 -0
  86. package/dist/features/blog/index.mjs.map +1 -0
  87. package/dist/features/blog/server.d.mts +36 -0
  88. package/dist/features/blog/server.d.ts +36 -0
  89. package/dist/features/blog/server.js +167 -0
  90. package/dist/features/blog/server.js.map +1 -0
  91. package/dist/features/blog/server.mjs +167 -0
  92. package/dist/features/blog/server.mjs.map +1 -0
  93. package/dist/features/help/index.js +38 -38
  94. package/dist/features/help/index.mjs +4 -4
  95. package/dist/features/tokenusage/index.d.mts +1 -3
  96. package/dist/features/tokenusage/index.d.ts +1 -3
  97. package/dist/features/tokenusage/index.js +127 -135
  98. package/dist/features/tokenusage/index.js.map +1 -1
  99. package/dist/features/tokenusage/index.mjs +10 -18
  100. package/dist/features/tokenusage/index.mjs.map +1 -1
  101. package/dist/index.d.mts +4 -2
  102. package/dist/index.d.ts +4 -2
  103. package/dist/index.js +39 -5
  104. package/dist/index.js.map +1 -1
  105. package/dist/index.mjs +38 -4
  106. package/dist/{s3.service-CY-jgOAM.d.ts → s3.service-Bz_K-tjz.d.ts} +11 -0
  107. package/dist/{s3.service-Sri0QP7Z.d.mts → s3.service-D5zNCEW3.d.mts} +11 -0
  108. package/dist/server/index.d.mts +1 -1
  109. package/dist/server/index.d.ts +1 -1
  110. package/dist/server/index.js +9 -7
  111. package/dist/server/index.js.map +1 -1
  112. package/dist/server/index.mjs +13 -11
  113. package/messages/en.json +93 -0
  114. package/messages/it.json +93 -0
  115. package/package.json +19 -1
  116. package/src/components/editors/BlockNoteEditor.tsx +13 -3
  117. package/src/components/editors/server/BlockNoteServerContent.tsx +28 -0
  118. package/src/components/editors/server/__tests__/renderBlockNoteHTML.spec.ts +44 -0
  119. package/src/components/editors/server/renderBlockNoteHTML.ts +26 -0
  120. package/src/components/editors/server-entry.ts +7 -0
  121. package/src/components/forms/FormBlockNote.tsx +4 -0
  122. package/src/components/index.ts +1 -0
  123. package/src/components/navigations/ContentTitle.tsx +14 -3
  124. package/src/components/viewers/BlockNoteStaticViewer.tsx +44 -0
  125. package/src/core/index.ts +4 -0
  126. package/src/core/registry/ModuleRegistry.ts +6 -0
  127. package/src/features/administration/components/AdminIndexContainer.tsx +61 -33
  128. package/src/features/administration/components/__tests__/AdminIndexContainer.spec.tsx +61 -1
  129. package/src/features/administration/components/index.ts +2 -1
  130. package/src/features/administration/contexts/AdministrationContext.tsx +11 -3
  131. package/src/features/administration/contexts/__tests__/AdministrationContext.spec.tsx +40 -0
  132. package/src/features/ai-connection/ai-connection.module.ts +2 -0
  133. package/src/features/ai-connection/contexts/AiConnectionsContext.tsx +3 -16
  134. package/src/features/ai-connection/contexts/__tests__/AiConnectionsContext.spec.tsx +33 -0
  135. package/src/features/analytics/__tests__/server-entry.spec.tsx +2 -1
  136. package/src/features/analytics/analytics.modules.ts +22 -5
  137. package/src/features/analytics/components/AnalyticsAdminContainer.tsx +4 -3
  138. package/src/features/analytics/components/AnalyticsAdminPageContainer.tsx +6 -3
  139. package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +4 -3
  140. package/src/features/analytics/components/__tests__/AnalyticsJourneyContainer.spec.tsx +11 -0
  141. package/src/features/analytics/components/__tests__/AnalyticsPageContainers.spec.tsx +12 -1
  142. package/src/features/analytics/components/server/AnalyticsAdminPage.tsx +6 -3
  143. package/src/features/analytics/contexts/AnalyticsAdminContext.tsx +8 -17
  144. package/src/features/analytics/contexts/__tests__/AnalyticsAdminContext.spec.tsx +17 -0
  145. package/src/features/billing/stripe-price/stripe-price.module.ts +2 -0
  146. package/src/features/billing/stripe-product/stripe-product.module.ts +2 -0
  147. package/src/features/blog/BlogModules.ts +49 -0
  148. package/src/features/blog/components/admin/BlogAdminContainer.tsx +39 -0
  149. package/src/features/blog/components/admin/BlogCategoriesList.tsx +56 -0
  150. package/src/features/blog/components/admin/BlogCategoryEditor.tsx +230 -0
  151. package/src/features/blog/components/admin/BlogCoverImageField.tsx +123 -0
  152. package/src/features/blog/components/admin/BlogLayoutSetting.tsx +86 -0
  153. package/src/features/blog/components/admin/BlogPostComparison.tsx +54 -0
  154. package/src/features/blog/components/admin/BlogPostContainer.tsx +101 -0
  155. package/src/features/blog/components/admin/BlogPostDeleter.tsx +35 -0
  156. package/src/features/blog/components/admin/BlogPostEditor.tsx +216 -0
  157. package/src/features/blog/components/admin/BlogPostPublishButton.tsx +50 -0
  158. package/src/features/blog/components/admin/BlogPostTranslationEditor.tsx +118 -0
  159. package/src/features/blog/components/admin/BlogPostTranslationPanel.tsx +136 -0
  160. package/src/features/blog/components/admin/BlogPostsList.tsx +58 -0
  161. package/src/features/blog/components/admin/BlogTranslationStatus.tsx +104 -0
  162. package/src/features/blog/components/admin/__tests__/BlogPostTranslationPanel.spec.tsx +100 -0
  163. package/src/features/blog/components/admin/__tests__/BlogPostsList.spec.tsx +30 -0
  164. package/src/features/blog/components/admin/index.ts +14 -0
  165. package/src/features/blog/components/public/BlogCategoryChips.tsx +59 -0
  166. package/src/features/blog/components/public/BlogList.tsx +21 -0
  167. package/src/features/blog/components/public/BlogListColumn.tsx +53 -0
  168. package/src/features/blog/components/public/BlogListFeatured.tsx +57 -0
  169. package/src/features/blog/components/public/BlogListParts.tsx +74 -0
  170. package/src/features/blog/components/public/BlogListRail.tsx +62 -0
  171. package/src/features/blog/components/public/BlogPostCard.tsx +125 -0
  172. package/src/features/blog/components/public/BlogPostMeta.tsx +49 -0
  173. package/src/features/blog/components/public/BlogPostView.tsx +80 -0
  174. package/src/features/blog/components/public/BlogSearchBox.tsx +52 -0
  175. package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +175 -0
  176. package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +134 -0
  177. package/src/features/blog/components/public/blogPublicHelpers.ts +30 -0
  178. package/src/features/blog/components/public/index.ts +3 -0
  179. package/src/features/blog/contexts/BlogPostContext.tsx +66 -0
  180. package/src/features/blog/contexts/index.ts +1 -0
  181. package/src/features/blog/data/BlogCategory.ts +93 -0
  182. package/src/features/blog/data/BlogCategoryFields.ts +4 -0
  183. package/src/features/blog/data/BlogCategoryInterface.ts +24 -0
  184. package/src/features/blog/data/BlogCategoryService.ts +54 -0
  185. package/src/features/blog/data/BlogCategoryTranslation.ts +90 -0
  186. package/src/features/blog/data/BlogCategoryTranslationInterface.ts +22 -0
  187. package/src/features/blog/data/BlogCategoryTranslationService.ts +30 -0
  188. package/src/features/blog/data/BlogPost.spec.ts +123 -0
  189. package/src/features/blog/data/BlogPost.ts +159 -0
  190. package/src/features/blog/data/BlogPostFields.ts +7 -0
  191. package/src/features/blog/data/BlogPostInterface.ts +39 -0
  192. package/src/features/blog/data/BlogPostService.ts +82 -0
  193. package/src/features/blog/data/BlogPostTranslation.spec.ts +73 -0
  194. package/src/features/blog/data/BlogPostTranslation.ts +110 -0
  195. package/src/features/blog/data/BlogPostTranslationInterface.ts +27 -0
  196. package/src/features/blog/data/BlogPostTranslationService.ts +30 -0
  197. package/src/features/blog/data/BlogSettings.ts +37 -0
  198. package/src/features/blog/data/BlogSettingsInterface.ts +11 -0
  199. package/src/features/blog/data/BlogSettingsService.ts +24 -0
  200. package/src/features/blog/data/index.ts +17 -0
  201. package/src/features/blog/hooks/index.ts +3 -0
  202. package/src/features/blog/hooks/useBlogCategoryNames.ts +34 -0
  203. package/src/features/blog/hooks/useBlogCategoryTableStructure.tsx +84 -0
  204. package/src/features/blog/hooks/useBlogPostTableStructure.tsx +126 -0
  205. package/src/features/blog/index.ts +15 -0
  206. package/src/features/blog/server/fetchers.ts +169 -0
  207. package/src/features/blog/server/toPublic.spec.ts +91 -0
  208. package/src/features/blog/server/toPublic.ts +36 -0
  209. package/src/features/blog/server-entry.ts +17 -0
  210. package/src/features/blog/types/blog-public.types.ts +34 -0
  211. package/src/features/blog/types/blog.constants.ts +40 -0
  212. package/src/features/email-automation/components/containers/EmailAutomationsListContainer.tsx +12 -3
  213. package/src/features/rbac/contexts/RbacContext.tsx +4 -7
  214. package/src/features/rbac/contexts/__tests__/RbacContext.spec.tsx +42 -0
  215. package/src/features/s3/data/s3.service.ts +17 -0
  216. package/src/features/tokenusage/components/TokenUsageAdminContainer.tsx +4 -3
  217. package/src/features/tokenusage/contexts/TokenUsageAdminContext.tsx +3 -18
  218. package/src/features/tokenusage/contexts/__tests__/TokenUsageAdminContext.spec.tsx +17 -0
  219. package/src/features/tokenusage/tokenusage-admin.module.ts +4 -3
  220. package/src/features/tokenusage/tokenusage.modules.ts +12 -2
  221. package/src/features/user/contexts/UserContext.tsx +18 -3
  222. package/src/features/user/contexts/__tests__/UserContext.spec.tsx +95 -0
  223. package/dist/BlockNoteEditor-UJ6363LC.js.map +0 -1
  224. package/dist/BlockNoteEditor-WTSQANKI.mjs.map +0 -1
  225. package/dist/chunk-3BAM23VA.js.map +0 -1
  226. package/dist/chunk-CRI4T47Y.js.map +0 -1
  227. package/dist/chunk-JQWI62NV.mjs.map +0 -1
  228. package/dist/chunk-MIZTWBPX.mjs.map +0 -1
  229. package/dist/chunk-QKZUE5OF.mjs.map +0 -1
  230. package/dist/chunk-XGTQWBDJ.js.map +0 -1
  231. /package/dist/{BlockNoteViewer-JVKWJZ6J.mjs.map → BlockNoteViewer-U6E3DS4F.mjs.map} +0 -0
  232. /package/dist/{chunk-6WU74VFU.mjs.map → chunk-6SQYXUVP.mjs.map} +0 -0
  233. /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
- const breadcrumbs: BreadcrumbItemData[] = [
98
- {
99
- name: t("ai_connections.admin.title"),
100
- href: generateUrl({ page: AI_CONNECTIONS_ADMIN_PAGE_URL }),
101
- },
102
- ];
87
+ // Unlinked: this provider only serves the AI-connections page, so the root is
88
+ // always the current page.
89
+ const breadcrumbs: BreadcrumbItemData[] = [{ name: t("ai_connections.admin.title") }];
103
90
 
104
91
  return (
105
92
  <SharedProvider value={{ breadcrumbs, title: { type: t("ai_connections.admin.title") } }}>
@@ -0,0 +1,33 @@
1
+ import { render, screen, waitFor } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { useSharedContext } from "../../../../contexts/SharedContext";
4
+ import { AiConnectionService } from "../../data";
5
+ import { AiConnectionsProvider } from "../AiConnectionsContext";
6
+
7
+ vi.mock("../../data", () => ({
8
+ AiConnectionService: {
9
+ listConnections: vi.fn(async () => ({ connections: [], providerRegistry: {}, envDefaults: {} })),
10
+ },
11
+ }));
12
+
13
+ // next-intl is globally mocked in vitest.setup.ts with `t = (key) => key`, so a
14
+ // translated label renders as its own key.
15
+ function BreadcrumbProbe() {
16
+ const { breadcrumbs } = useSharedContext();
17
+ return <span data-testid="breadcrumbs">{JSON.stringify(breadcrumbs)}</span>;
18
+ }
19
+
20
+ describe("AiConnectionsContext", () => {
21
+ it("publishes the page as an unlinked breadcrumb, since it is the current page", async () => {
22
+ render(
23
+ <AiConnectionsProvider>
24
+ <BreadcrumbProbe />
25
+ </AiConnectionsProvider>,
26
+ );
27
+
28
+ await waitFor(() => expect(AiConnectionService.listConnections).toHaveBeenCalled());
29
+ expect(screen.getByTestId("breadcrumbs")).toHaveTextContent(
30
+ JSON.stringify([{ name: "ai_connections.admin.title" }]),
31
+ );
32
+ });
33
+ });
@@ -35,7 +35,8 @@ describe("analytics server entry", () => {
35
35
 
36
36
  expect(mocks.hasRole).toHaveBeenCalledWith("role-admin");
37
37
  expect(element.type).toBe(AnalyticsAdminPageContainer);
38
- expect(element.props).toEqual({ pageUrl: "/admin/stats", sessionPageUrl: "/admin/stats/sessions" });
38
+ // `pageUrl` is deprecated and no longer forwarded: the dashboard breadcrumb is unlinked.
39
+ expect(element.props).toEqual({ sessionPageUrl: "/admin/stats/sessions" });
39
40
  });
40
41
 
41
42
  it("AnalyticsAdminPage redirects to `/` by default without the role", async () => {
@@ -1,3 +1,4 @@
1
+ import { ChartNoAxesCombinedIcon } from "lucide-react";
1
2
  import { ModuleFactory, ModuleWithPermissions } from "../../permissions";
2
3
  import { AnalyticsBreakdown } from "./data/analytics-breakdown";
3
4
  import { AnalyticsEvent } from "./data/analytics-event";
@@ -22,9 +23,25 @@ import { AnalyticsTimeline } from "./data/analytics-timeline";
22
23
  export const analyticsModules = (factory: ModuleFactory) =>
23
24
  ({
24
25
  AnalyticsEvent: factory({ name: "analytics/events", model: AnalyticsEvent }),
25
- AnalyticsSummary: factory({ name: "analytics/administration/summary", model: AnalyticsSummary }),
26
- AnalyticsTimeline: factory({ name: "analytics/administration/timeline", model: AnalyticsTimeline }),
27
- AnalyticsBreakdown: factory({ name: "analytics/administration/breakdown", model: AnalyticsBreakdown }),
28
- AnalyticsSession: factory({ name: "analytics/administration/sessions", model: AnalyticsSession }),
29
- AnalyticsPageView: factory({ name: "pageviews", model: AnalyticsPageView }),
26
+ AnalyticsSummary: factory({
27
+ name: "analytics/administration/summary",
28
+ model: AnalyticsSummary,
29
+ icon: ChartNoAxesCombinedIcon,
30
+ }),
31
+ AnalyticsTimeline: factory({
32
+ name: "analytics/administration/timeline",
33
+ model: AnalyticsTimeline,
34
+ icon: ChartNoAxesCombinedIcon,
35
+ }),
36
+ AnalyticsBreakdown: factory({
37
+ name: "analytics/administration/breakdown",
38
+ model: AnalyticsBreakdown,
39
+ icon: ChartNoAxesCombinedIcon,
40
+ }),
41
+ AnalyticsSession: factory({
42
+ name: "analytics/administration/sessions",
43
+ model: AnalyticsSession,
44
+ icon: ChartNoAxesCombinedIcon,
45
+ }),
46
+ AnalyticsPageView: factory({ name: "pageviews", model: AnalyticsPageView, icon: ChartNoAxesCombinedIcon }),
30
47
  }) satisfies Record<string, ModuleWithPermissions>;
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useTranslations } from "next-intl";
4
4
  import { RoundPageContainer } from "../../../components";
5
+ import { Modules } from "../../../core";
5
6
  import { Card, CardContent, CardHeader, CardTitle } from "../../../shadcnui";
6
7
  import { useAnalyticsAdmin } from "../contexts/AnalyticsAdminContext";
7
8
  import { AnalyticsAdminTiles } from "./AnalyticsAdminTiles";
@@ -33,7 +34,7 @@ export function AnalyticsAdminContainer() {
33
34
 
34
35
  if (error) {
35
36
  return (
36
- <RoundPageContainer fullWidth forceHeader>
37
+ <RoundPageContainer module={Modules.AnalyticsSummary} fullWidth forceHeader>
37
38
  <div className="p-4">
38
39
  <Card>
39
40
  <CardContent>
@@ -47,10 +48,10 @@ export function AnalyticsAdminContainer() {
47
48
 
48
49
  // Loading renders nothing in the body: the title bar (with the filter bar) is
49
50
  // already mounted, so a spinner would only make the controls jump on arrival.
50
- if (isLoading) return <RoundPageContainer fullWidth forceHeader />;
51
+ if (isLoading) return <RoundPageContainer module={Modules.AnalyticsSummary} fullWidth forceHeader />;
51
52
 
52
53
  return (
53
- <RoundPageContainer fullWidth forceHeader>
54
+ <RoundPageContainer module={Modules.AnalyticsSummary} fullWidth forceHeader>
54
55
  <div className="flex w-full flex-col gap-4 p-4">
55
56
  <div className="flex flex-col gap-2">
56
57
  <AnalyticsAdminTiles summary={summary} />
@@ -4,7 +4,10 @@ import { AnalyticsAdminProvider } from "../contexts/AnalyticsAdminContext";
4
4
  import { AnalyticsAdminContainer } from "./AnalyticsAdminContainer";
5
5
 
6
6
  type Props = {
7
- /** Route the breadcrumb links back to. Defaults to the provider's default. */
7
+ /**
8
+ * @deprecated No longer read: the dashboard is the current page, so its
9
+ * breadcrumb is not linked. Kept so existing callers still compile.
10
+ */
8
11
  pageUrl?: string;
9
12
  /** Base route of the journey page each session row links to. */
10
13
  sessionPageUrl?: string;
@@ -16,9 +19,9 @@ type Props = {
16
19
  * the `RoundPageContainer`; the provider sits above it because it publishes the
17
20
  * filter bar into the title bar.
18
21
  */
19
- export function AnalyticsAdminPageContainer({ pageUrl, sessionPageUrl }: Props) {
22
+ export function AnalyticsAdminPageContainer({ sessionPageUrl }: Props) {
20
23
  return (
21
- <AnalyticsAdminProvider pageUrl={pageUrl} sessionPageUrl={sessionPageUrl}>
24
+ <AnalyticsAdminProvider sessionPageUrl={sessionPageUrl}>
22
25
  <AnalyticsAdminContainer />
23
26
  </AnalyticsAdminProvider>
24
27
  );
@@ -4,6 +4,7 @@ import { useLocale, useTranslations } from "next-intl";
4
4
  import { useEffect, useMemo, useState } from "react";
5
5
  import { DetailField, RoundPageContainer } from "../../../components";
6
6
  import { SharedProvider } from "../../../contexts";
7
+ import { Modules } from "../../../core";
7
8
  import { usePageUrlGenerator } from "../../../hooks";
8
9
  import { BreadcrumbItemData } from "../../../interfaces";
9
10
  import {
@@ -125,7 +126,7 @@ export function AnalyticsJourneyContainer({ sessionId, pageUrl = ANALYTICS_ADMIN
125
126
  const renderBody = () => {
126
127
  if (error) {
127
128
  return (
128
- <RoundPageContainer fullWidth forceHeader>
129
+ <RoundPageContainer module={Modules.AnalyticsSession} fullWidth forceHeader>
129
130
  <div className="p-4">
130
131
  <Card>
131
132
  <CardContent>
@@ -138,14 +139,14 @@ export function AnalyticsJourneyContainer({ sessionId, pageUrl = ANALYTICS_ADMIN
138
139
  }
139
140
 
140
141
  // Loading renders nothing in the body, as on the dashboard.
141
- if (isLoading || !session) return <RoundPageContainer fullWidth forceHeader />;
142
+ if (isLoading || !session) return <RoundPageContainer module={Modules.AnalyticsSession} fullWidth forceHeader />;
142
143
 
143
144
  const source = [session.utmSource ?? session.referrerHost, session.utmMedium, session.utmCampaign]
144
145
  .filter(Boolean)
145
146
  .join(" / ");
146
147
 
147
148
  return (
148
- <RoundPageContainer fullWidth forceHeader>
149
+ <RoundPageContainer module={Modules.AnalyticsSession} fullWidth forceHeader>
149
150
  <div className="flex w-full flex-col gap-4 p-4">
150
151
  <Card data-testid="analytics-journey-attribution">
151
152
  <CardContent className="flex flex-col gap-3">
@@ -22,6 +22,17 @@ vi.mock("../../../../components", async (importOriginal) => {
22
22
  };
23
23
  });
24
24
 
25
+ // The container hands `Modules.AnalyticsSession` to the (mocked) page chrome for
26
+ // its header icon; no app bootstrap registers modules here, so the registry
27
+ // lookup would throw.
28
+ vi.mock("../../../../core", async (importOriginal) => {
29
+ const actual = await importOriginal<typeof import("../../../../core")>();
30
+ return {
31
+ ...actual,
32
+ Modules: new Proxy({}, { get: () => undefined }),
33
+ };
34
+ });
35
+
25
36
  beforeAll(() => {
26
37
  configureI18n({
27
38
  useRouter: () => ({
@@ -27,6 +27,17 @@ vi.mock("../../../../components", async (importOriginal) => {
27
27
  };
28
28
  });
29
29
 
30
+ // The containers pass Modules.AnalyticsSummary / AnalyticsSession for the
31
+ // header icon; no app bootstrap registers modules here, so the registry lookup
32
+ // would throw.
33
+ vi.mock("../../../../core", async (importOriginal) => {
34
+ const actual = await importOriginal<typeof import("../../../../core")>();
35
+ return {
36
+ ...actual,
37
+ Modules: new Proxy({}, { get: () => undefined }),
38
+ };
39
+ });
40
+
30
41
  beforeAll(() => {
31
42
  configureI18n({
32
43
  useRouter: () => ({
@@ -81,7 +92,7 @@ describe("AnalyticsAdminPageContainer", () => {
81
92
  });
82
93
 
83
94
  it("loads the dashboard and links each session under the given sessionPageUrl", async () => {
84
- render(<AnalyticsAdminPageContainer pageUrl="/admin/stats" sessionPageUrl="/admin/stats/sessions" />);
95
+ render(<AnalyticsAdminPageContainer sessionPageUrl="/admin/stats/sessions" />);
85
96
 
86
97
  expect(await screen.findByTestId("analytics-session-link-s1")).toHaveAttribute("href", "/admin/stats/sessions/s1");
87
98
  expect(AnalyticsAdminService.getSummary).toHaveBeenCalledTimes(1);
@@ -10,7 +10,10 @@ import { ServerSession } from "../../../../server";
10
10
  type Props = {
11
11
  /** Role id the viewer must hold. The package has no role constants, so the host app passes its own. */
12
12
  adminRoleId: string;
13
- /** Route the breadcrumb links back to. */
13
+ /**
14
+ * @deprecated No longer read: the dashboard is the current page, so its
15
+ * breadcrumb is not linked. Kept so existing callers still compile.
16
+ */
14
17
  pageUrl?: string;
15
18
  /** Base route of the journey page each session row links to. */
16
19
  sessionPageUrl?: string;
@@ -23,10 +26,10 @@ type Props = {
23
26
  *
24
27
  * Reads the session: render inside a `<Suspense>` boundary (Cache Components).
25
28
  */
26
- export async function AnalyticsAdminPage({ adminRoleId, pageUrl, sessionPageUrl, deniedRedirect = "/" }: Props) {
29
+ export async function AnalyticsAdminPage({ adminRoleId, sessionPageUrl, deniedRedirect = "/" }: Props) {
27
30
  if (!(await ServerSession.hasRole(adminRoleId))) {
28
31
  redirect(deniedRedirect);
29
32
  }
30
33
 
31
- return <AnalyticsAdminPageContainer pageUrl={pageUrl} sessionPageUrl={sessionPageUrl} />;
34
+ return <AnalyticsAdminPageContainer sessionPageUrl={sessionPageUrl} />;
32
35
  }
@@ -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
- /** Route the breadcrumb links back to. */
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
- const breadcrumb = (): BreadcrumbItemData[] => [
209
- {
210
- name: t("analytics.admin.title"),
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
+ });