@nocobase/portal-template-default 1.0.3 → 1.0.5

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 (170) hide show
  1. package/README.MD +10 -5
  2. package/package.json +17 -17
  3. package/registry/nocobase-ai/components/chat/chat-message.tsx +8 -6
  4. package/registry/nocobase-ai/components/chat/chat-messages.tsx +8 -3
  5. package/registry/nocobase-ai/components/chat/markdown-message.tsx +26 -0
  6. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +3 -3
  7. package/registry/nocobase-ai/demo/configuration-gate.tsx +83 -35
  8. package/registry/nocobase-i18n/README.md +23 -32
  9. package/registry/nocobase-i18n/components/language-switcher.tsx +1 -1
  10. package/registry/nocobase-i18n/demo/index.tsx +2 -2
  11. package/registry/nocobase-i18n/demo/prompt-generator.tsx +3 -2
  12. package/registry/nocobase-i18n/extension.tsx +3 -4
  13. package/registry/nocobase-i18n/index.ts +4 -3
  14. package/registry/nocobase-i18n/locales/en-US.ts +2 -52
  15. package/registry/nocobase-i18n/locales/index.ts +8 -0
  16. package/registry/nocobase-i18n/locales/zh-CN.ts +2 -52
  17. package/registry/nocobase-i18n/provider.tsx +18 -6
  18. package/registry/nocobase-i18n/server-resources.ts +13 -31
  19. package/registry/nocobase-i18n/tests/i18n-regression.mjs +14 -5
  20. package/registry/nocobase-route-surfaces/README.md +21 -0
  21. package/registry/nocobase-route-surfaces/demo/index.tsx +266 -0
  22. package/{src/extensions/nocobase-acl → registry/nocobase-route-surfaces}/demo/lazy-route.tsx +2 -1
  23. package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +90 -0
  24. package/registry/nocobase-route-surfaces/demo/scenarios.ts +95 -0
  25. package/registry/nocobase-route-surfaces/extension.tsx +119 -0
  26. package/registry/nocobase-route-surfaces/index.ts +7 -0
  27. package/registry/nocobase-route-surfaces/route-dialog.tsx +145 -0
  28. package/registry/nocobase-route-surfaces/route-drawer.tsx +186 -0
  29. package/registry/nocobase-route-surfaces/route-page.tsx +29 -0
  30. package/registry/nocobase-route-surfaces/route-surface-context.ts +13 -0
  31. package/registry/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +106 -0
  32. package/registry/nocobase-route-surfaces/use-route-surface-close.ts +15 -0
  33. package/registry/nocobase-route-surfaces/use-route-surface-state.ts +65 -0
  34. package/registry.config.json +30 -3
  35. package/scripts/registry.mjs +0 -2
  36. package/src/App.tsx +65 -35
  37. package/src/app/extension.ts +1 -2
  38. package/src/app/extensions.tsx +0 -14
  39. package/src/components/app-shell/breadcrumb.tsx +67 -2
  40. package/src/components/app-shell/sidebar.tsx +2 -2
  41. package/src/components/resources/views/list-view.tsx +3 -2
  42. package/src/components/ui/sheet.tsx +22 -21
  43. package/src/lib/nocobase/client.ts +6 -1
  44. package/src/locales/en-US.ts +81 -0
  45. package/src/locales/zh-CN.ts +77 -0
  46. package/src/pages/users/create.tsx +76 -53
  47. package/src/pages/users/edit.tsx +87 -56
  48. package/src/pages/users/index.ts +3 -0
  49. package/src/pages/users/layout.tsx +15 -0
  50. package/src/pages/users/list.tsx +19 -21
  51. package/src/pages/users/role-badges.tsx +43 -0
  52. package/src/pages/users/role-detail.tsx +272 -0
  53. package/src/pages/users/role-utils.ts +6 -0
  54. package/src/pages/users/routes.ts +24 -0
  55. package/src/pages/users/show.tsx +195 -104
  56. package/src/pages/users/types.ts +13 -0
  57. package/src/providers/i18n/index.ts +3 -0
  58. package/{registry/nocobase-i18n → src/providers/i18n}/runtime.ts +63 -16
  59. package/src/providers/system-settings/context.ts +26 -0
  60. package/src/providers/system-settings/index.ts +2 -0
  61. package/src/providers/system-settings/provider.tsx +94 -0
  62. package/src/extensions/nocobase-acl/README.md +0 -19
  63. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +0 -106
  64. package/src/extensions/nocobase-acl/components/index.ts +0 -3
  65. package/src/extensions/nocobase-acl/components/role-options.ts +0 -36
  66. package/src/extensions/nocobase-acl/components/role-switcher.tsx +0 -135
  67. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +0 -186
  68. package/src/extensions/nocobase-acl/demo/components.tsx +0 -231
  69. package/src/extensions/nocobase-acl/demo/index.tsx +0 -505
  70. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +0 -134
  71. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +0 -198
  72. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +0 -37
  73. package/src/extensions/nocobase-acl/extension.tsx +0 -71
  74. package/src/extensions/nocobase-acl/index.ts +0 -1
  75. package/src/extensions/nocobase-ai/README.md +0 -70
  76. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +0 -20
  77. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +0 -50
  78. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +0 -32
  79. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +0 -31
  80. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +0 -92
  81. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +0 -90
  82. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +0 -429
  83. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +0 -51
  84. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +0 -88
  85. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +0 -77
  86. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +0 -252
  87. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +0 -173
  88. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +0 -118
  89. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +0 -343
  90. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +0 -165
  91. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +0 -30
  92. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +0 -29
  93. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +0 -142
  94. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +0 -316
  95. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +0 -99
  96. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +0 -114
  97. package/src/extensions/nocobase-ai/components/index.ts +0 -59
  98. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +0 -44
  99. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +0 -469
  100. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +0 -31
  101. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +0 -18
  102. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +0 -18
  103. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +0 -54
  104. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +0 -74
  105. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +0 -54
  106. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +0 -31
  107. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +0 -364
  108. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +0 -122
  109. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +0 -282
  110. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +0 -99
  111. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +0 -59
  112. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +0 -18
  113. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +0 -20
  114. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +0 -34
  115. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +0 -24
  116. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +0 -138
  117. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +0 -67
  118. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +0 -59
  119. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +0 -55
  120. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +0 -18
  121. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +0 -108
  122. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +0 -73
  123. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +0 -149
  124. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +0 -49
  125. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +0 -210
  126. package/src/extensions/nocobase-ai/demo/floating.tsx +0 -134
  127. package/src/extensions/nocobase-ai/demo/index.tsx +0 -346
  128. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +0 -151
  129. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +0 -355
  130. package/src/extensions/nocobase-ai/demo/page-context.tsx +0 -884
  131. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +0 -197
  132. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +0 -41
  133. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +0 -452
  134. package/src/extensions/nocobase-ai/demo/shortcut.tsx +0 -1207
  135. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +0 -530
  136. package/src/extensions/nocobase-ai/extension.tsx +0 -158
  137. package/src/extensions/nocobase-ai/global-ai-chat.tsx +0 -213
  138. package/src/extensions/nocobase-ai/index.ts +0 -3
  139. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +0 -323
  140. package/src/extensions/nocobase-ai/providers/avatars.ts +0 -817
  141. package/src/extensions/nocobase-ai/providers/chat-context.tsx +0 -109
  142. package/src/extensions/nocobase-ai/providers/chat-controller.ts +0 -84
  143. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +0 -198
  144. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +0 -871
  145. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +0 -147
  146. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +0 -105
  147. package/src/extensions/nocobase-ai/providers/chat-transport.ts +0 -184
  148. package/src/extensions/nocobase-ai/providers/form-registry.tsx +0 -271
  149. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +0 -231
  150. package/src/extensions/nocobase-ai/providers/index.ts +0 -61
  151. package/src/extensions/nocobase-ai/providers/model.ts +0 -32
  152. package/src/extensions/nocobase-ai/providers/page-context.tsx +0 -136
  153. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +0 -60
  154. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +0 -82
  155. package/src/extensions/nocobase-ai/providers/stream-parser.ts +0 -61
  156. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +0 -316
  157. package/src/extensions/nocobase-ai/providers/types.ts +0 -183
  158. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +0 -463
  159. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +0 -94
  160. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +0 -206
  161. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +0 -554
  162. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +0 -210
  163. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +0 -93
  164. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +0 -108
  165. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +0 -178
  166. package/src/extensions/nocobase-ai/services/index.ts +0 -7
  167. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +0 -454
  168. package/src/extensions/nocobase-ai/services/types.ts +0 -82
  169. /package/{registry/nocobase-i18n → src/providers/i18n}/i18n-provider.ts +0 -0
  170. /package/{registry/nocobase-i18n → src/providers/i18n}/locale-store.ts +0 -0
@@ -0,0 +1,106 @@
1
+ import { useTranslate, useWarnAboutChange } from "@refinedev/core";
2
+ import { TriangleAlert } from "lucide-react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+
5
+ import {
6
+ AlertDialog,
7
+ AlertDialogAction,
8
+ AlertDialogCancel,
9
+ AlertDialogContent,
10
+ AlertDialogDescription,
11
+ AlertDialogFooter,
12
+ AlertDialogHeader,
13
+ AlertDialogMedia,
14
+ AlertDialogTitle,
15
+ } from "@/components/ui/alert-dialog";
16
+
17
+ type PendingConfirmation = {
18
+ promise: Promise<boolean>;
19
+ resolve: (allowed: boolean) => void;
20
+ };
21
+
22
+ export function useRefineUnsavedChangesGuard() {
23
+ const translate = useTranslate();
24
+ const { warnWhen, setWarnWhen } = useWarnAboutChange();
25
+ const [open, setOpen] = useState(false);
26
+ const pendingRef = useRef<PendingConfirmation | null>(null);
27
+
28
+ const settle = useCallback(
29
+ (allowed: boolean) => {
30
+ const pending = pendingRef.current;
31
+ if (!pending) return;
32
+
33
+ pendingRef.current = null;
34
+ if (allowed) setWarnWhen(false);
35
+ setOpen(false);
36
+ pending.resolve(allowed);
37
+ },
38
+ [setWarnWhen]
39
+ );
40
+
41
+ useEffect(
42
+ () => () => {
43
+ pendingRef.current?.resolve(false);
44
+ pendingRef.current = null;
45
+ },
46
+ []
47
+ );
48
+
49
+ const beforeClose = useCallback(() => {
50
+ if (!warnWhen) return true;
51
+ if (pendingRef.current) return pendingRef.current.promise;
52
+
53
+ let resolve!: (allowed: boolean) => void;
54
+ const promise = new Promise<boolean>((next) => {
55
+ resolve = next;
56
+ });
57
+ pendingRef.current = { promise, resolve };
58
+ setOpen(true);
59
+ return promise;
60
+ }, [warnWhen]);
61
+
62
+ const confirmation = useMemo(
63
+ () => (
64
+ <AlertDialog
65
+ open={open}
66
+ onOpenChange={(nextOpen) => {
67
+ if (!nextOpen) settle(false);
68
+ }}
69
+ >
70
+ <AlertDialogContent>
71
+ <AlertDialogHeader>
72
+ <AlertDialogMedia>
73
+ <TriangleAlert />
74
+ </AlertDialogMedia>
75
+ <AlertDialogTitle>
76
+ {translate(
77
+ "unsavedChanges.title",
78
+ "Discard unsaved changes?"
79
+ )}
80
+ </AlertDialogTitle>
81
+ <AlertDialogDescription>
82
+ {translate(
83
+ "unsavedChanges.description",
84
+ "Your changes have not been saved. Leaving now will discard them."
85
+ )}
86
+ </AlertDialogDescription>
87
+ </AlertDialogHeader>
88
+ <AlertDialogFooter>
89
+ <AlertDialogCancel onClick={() => settle(false)}>
90
+ {translate("unsavedChanges.stay", "Keep editing")}
91
+ </AlertDialogCancel>
92
+ <AlertDialogAction
93
+ variant="destructive"
94
+ onClick={() => settle(true)}
95
+ >
96
+ {translate("unsavedChanges.leave", "Discard changes")}
97
+ </AlertDialogAction>
98
+ </AlertDialogFooter>
99
+ </AlertDialogContent>
100
+ </AlertDialog>
101
+ ),
102
+ [open, settle, translate]
103
+ );
104
+
105
+ return { beforeClose, confirmation };
106
+ }
@@ -0,0 +1,15 @@
1
+ import { useContext } from "react";
2
+
3
+ import { RouteSurfaceContext } from "./route-surface-context";
4
+
5
+ export function useRouteSurfaceClose() {
6
+ const close = useContext(RouteSurfaceContext);
7
+
8
+ if (!close) {
9
+ throw new Error(
10
+ "useRouteSurfaceClose must be used inside a RouteDrawer, RouteDialog, or RoutePage."
11
+ );
12
+ }
13
+
14
+ return close;
15
+ }
@@ -0,0 +1,65 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { useNavigate } from "react-router";
3
+
4
+ import type {
5
+ RouteSurfaceClose,
6
+ RouteSurfaceCloseOptions,
7
+ } from "./route-surface-context";
8
+
9
+ export type RouteSurfaceBeforeClose = () => boolean | Promise<boolean>;
10
+
11
+ export function useRouteSurfaceState({
12
+ closeTo,
13
+ beforeClose,
14
+ animated,
15
+ }: {
16
+ closeTo: string;
17
+ beforeClose?: RouteSurfaceBeforeClose;
18
+ animated: boolean;
19
+ }) {
20
+ const navigate = useNavigate();
21
+ const [open, setOpen] = useState(!animated);
22
+ const closingRef = useRef(false);
23
+
24
+ useEffect(() => {
25
+ if (!animated) return;
26
+
27
+ const frame = requestAnimationFrame(() => setOpen(true));
28
+ return () => cancelAnimationFrame(frame);
29
+ }, [animated]);
30
+
31
+ const navigateAfterClose = useCallback(() => {
32
+ navigate(closeTo, { replace: true });
33
+ }, [closeTo, navigate]);
34
+
35
+ const close = useCallback<RouteSurfaceClose>(
36
+ async (options?: RouteSurfaceCloseOptions) => {
37
+ if (closingRef.current) return false;
38
+
39
+ closingRef.current = true;
40
+ try {
41
+ if (!options?.skipBeforeClose && beforeClose) {
42
+ const allowed = await beforeClose();
43
+ if (!allowed) return false;
44
+ }
45
+
46
+ if (animated) {
47
+ setOpen(false);
48
+ } else {
49
+ navigateAfterClose();
50
+ }
51
+ return true;
52
+ } finally {
53
+ closingRef.current = false;
54
+ }
55
+ },
56
+ [animated, beforeClose, navigateAfterClose]
57
+ );
58
+
59
+ return {
60
+ open,
61
+ setOpen,
62
+ close,
63
+ navigateAfterClose,
64
+ };
65
+ }
@@ -2,14 +2,41 @@
2
2
  "name": "nocobase-ai",
3
3
  "homepage": "https://www.nocobase.com",
4
4
  "items": [
5
+ {
6
+ "name": "route-surfaces",
7
+ "type": "registry:lib",
8
+ "title": "NocoBase Route Surfaces",
9
+ "description": "URL-backed drawer, dialog, and page containers with nested drawer stacking and a shared close lifecycle.",
10
+ "dependencies": [
11
+ "@base-ui/react@^1.6.0",
12
+ "@refinedev/core@^5.0.8",
13
+ "lucide-react@^0.487.0",
14
+ "react-router@^7.0.2"
15
+ ],
16
+ "registryDependencies": [
17
+ "alert-dialog",
18
+ "badge",
19
+ "button",
20
+ "card",
21
+ "input",
22
+ "label",
23
+ "select",
24
+ "textarea"
25
+ ],
26
+ "docs": "Provides presentation containers plus an integrated Demo and Prompt generator. Route definitions, ACL guards, data fetching, and business content remain application-owned.",
27
+ "source": {
28
+ "root": "registry/nocobase-route-surfaces",
29
+ "target": "src/extensions/nocobase-route-surfaces",
30
+ "include": ["."]
31
+ }
32
+ },
5
33
  {
6
34
  "name": "i18n",
7
35
  "type": "registry:block",
8
36
  "title": "NocoBase i18n",
9
- "description": "Optional hybrid internationalization with frontend-owned UI resources, selected NocoBase dynamic namespaces, X-Locale synchronization, a reusable language switcher, and an integrated showcase.",
37
+ "description": "Optional NocoBase server-language resources, user preference persistence, reusable language controls, and an integrated showcase for the Starter's built-in i18n runtime.",
10
38
  "dependencies": [
11
39
  "@refinedev/core@^5.0.8",
12
- "i18next@^25.3.2",
13
40
  "lucide-react@^0.487.0",
14
41
  "react-router@^7.0.2"
15
42
  ],
@@ -23,7 +50,7 @@
23
50
  "select",
24
51
  "textarea"
25
52
  ],
26
- "docs": "Requires a compatible NocoBase Starter with the generic AppExtension i18nProvider and UserMenuItems host fields. Starter and Registry translations stay local; app:getLang is used only to merge registered dynamic namespaces such as lm-collections. The integrated Demo route documents the complete usage model.",
53
+ "docs": "Requires a compatible NocoBase Starter with the built-in i18n provider, shared System Settings bootstrap, and AppExtension Provider/UserMenuItems host fields. The Registry reuses systemSettings:get data and calls app:getLang only for registered dynamic namespaces such as lm-collections.",
27
54
  "source": {
28
55
  "root": "registry/nocobase-i18n",
29
56
  "target": "src/extensions/nocobase-i18n",
@@ -2,8 +2,6 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
- console.log(`Running registry script with action: ${process.argv[2]}`);
6
-
7
5
  const projectRoot = path.resolve(
8
6
  path.dirname(fileURLToPath(import.meta.url)),
9
7
  ".."
package/src/App.tsx CHANGED
@@ -5,7 +5,14 @@ import routerProvider, {
5
5
  CatchAllNavigate,
6
6
  UnsavedChangesNotifier,
7
7
  } from "@refinedev/react-router";
8
- import { UserCreate, UserEdit, UserList, UserShow } from "./pages/users";
8
+ import {
9
+ UserCreate,
10
+ UserEdit,
11
+ UserResourceLayout,
12
+ RoleDetailRoute,
13
+ userRoutes,
14
+ UserShow,
15
+ } from "./pages/users";
9
16
  import { dataProvider } from "./providers/data";
10
17
  import { Login } from "./pages/login";
11
18
  import { Register } from "./pages/register";
@@ -20,7 +27,6 @@ import { TooltipProvider } from "./components/ui/tooltip";
20
27
  import { BrandLogo } from "./components/app-shell/brand";
21
28
  import {
22
29
  AppExtensionProviders,
23
- extensionI18nProvider,
24
30
  extensionResources,
25
31
  extensionRouteElements,
26
32
  } from "./app/extensions";
@@ -31,14 +37,16 @@ import { AclBootstrap } from "./components/access-control/acl-bootstrap";
31
37
  import { ResourceAccessGuard } from "./components/access-control/resource-access-guard";
32
38
  import { NavigateToAccessibleResource } from "./components/access-control/navigate-to-accessible-resource";
33
39
  import { UsersRound } from "lucide-react";
40
+ import { i18nProvider } from "./providers/i18n";
41
+ import { SystemSettingsProvider } from "./providers/system-settings";
34
42
 
35
43
  const coreResources: ResourceProps[] = [
36
44
  {
37
45
  name: "users",
38
- list: "/users",
39
- create: "/users/create",
40
- edit: "/users/edit/:id",
41
- show: "/users/show/:id",
46
+ list: userRoutes.list,
47
+ create: userRoutes.create,
48
+ edit: userRoutes.edit,
49
+ show: userRoutes.show,
42
50
  meta: {
43
51
  label: "Users",
44
52
  singularLabel: "User",
@@ -65,24 +73,25 @@ function App() {
65
73
  <BrowserRouter basename={basename || undefined}>
66
74
  <ThemeProvider>
67
75
  <TooltipProvider>
68
- <Refine
69
- dataProvider={dataProvider}
70
- notificationProvider={useNotificationProvider()}
71
- routerProvider={routerProvider}
72
- authProvider={authProvider}
73
- accessControlProvider={accessControlProvider}
74
- i18nProvider={extensionI18nProvider}
75
- resources={[...coreResources, ...extensionResources]}
76
- options={{
77
- syncWithLocation: true,
78
- warnWhenUnsavedChanges: true,
79
- disableTelemetry: true,
80
- title: {
81
- text: "NocoBase",
82
- icon: <BrandLogo className="size-14 rounded-2xl" />,
83
- },
84
- }}
85
- >
76
+ <SystemSettingsProvider>
77
+ <Refine
78
+ dataProvider={dataProvider}
79
+ notificationProvider={useNotificationProvider()}
80
+ routerProvider={routerProvider}
81
+ authProvider={authProvider}
82
+ accessControlProvider={accessControlProvider}
83
+ i18nProvider={i18nProvider}
84
+ resources={[...coreResources, ...extensionResources]}
85
+ options={{
86
+ syncWithLocation: true,
87
+ warnWhenUnsavedChanges: true,
88
+ disableTelemetry: true,
89
+ title: {
90
+ text: "NocoBase",
91
+ icon: <BrandLogo className="size-14 rounded-2xl" />,
92
+ },
93
+ }}
94
+ >
86
95
  <Routes>
87
96
  <Route
88
97
  element={
@@ -101,15 +110,10 @@ function App() {
101
110
  }
102
111
  >
103
112
  <Route index element={<NavigateToAccessibleResource />} />
104
- <Route path="/users">
105
- <Route
106
- index
107
- element={
108
- <ResourceAccessGuard resource="users" action="list">
109
- <UserList />
110
- </ResourceAccessGuard>
111
- }
112
- />
113
+ <Route
114
+ path="/users"
115
+ element={<UserResourceLayout />}
116
+ >
113
117
  <Route
114
118
  path="create"
115
119
  element={
@@ -126,6 +130,14 @@ function App() {
126
130
  </ResourceAccessGuard>
127
131
  }
128
132
  />
133
+ <Route
134
+ path="roles/:roleName"
135
+ element={
136
+ <ResourceAccessGuard resource="roles" action="show">
137
+ <RoleDetailRoute returnTo="list" />
138
+ </ResourceAccessGuard>
139
+ }
140
+ />
129
141
  <Route
130
142
  path="show/:id"
131
143
  element={
@@ -133,7 +145,24 @@ function App() {
133
145
  <UserShow />
134
146
  </ResourceAccessGuard>
135
147
  }
136
- />
148
+ >
149
+ <Route
150
+ path="edit"
151
+ element={
152
+ <ResourceAccessGuard resource="users" action="edit">
153
+ <UserEdit returnTo="show" />
154
+ </ResourceAccessGuard>
155
+ }
156
+ />
157
+ <Route
158
+ path="roles/:roleName"
159
+ element={
160
+ <ResourceAccessGuard resource="roles" action="show">
161
+ <RoleDetailRoute returnTo="show" />
162
+ </ResourceAccessGuard>
163
+ }
164
+ />
165
+ </Route>
137
166
  </Route>
138
167
  {extensionRouteElements}
139
168
  <Route path="*" element={<ErrorComponent />} />
@@ -159,7 +188,8 @@ function App() {
159
188
  <Toaster />
160
189
  <UnsavedChangesNotifier />
161
190
  <DocumentTitleHandler appName="NocoBase" />
162
- </Refine>
191
+ </Refine>
192
+ </SystemSettingsProvider>
163
193
  </TooltipProvider>
164
194
  </ThemeProvider>
165
195
  </BrowserRouter>
@@ -1,4 +1,4 @@
1
- import type { I18nProvider, ResourceProps } from "@refinedev/core";
1
+ import type { ResourceProps } from "@refinedev/core";
2
2
  import type { ComponentType, PropsWithChildren, ReactElement } from "react";
3
3
 
4
4
  export type AppExtension = {
@@ -7,5 +7,4 @@ export type AppExtension = {
7
7
  routes?: ReactElement;
8
8
  Provider?: ComponentType<PropsWithChildren>;
9
9
  UserMenuItems?: ComponentType;
10
- i18nProvider?: I18nProvider;
11
10
  };
@@ -18,20 +18,6 @@ export const extensionRouteElements = appExtensions
18
18
  .map((extension) => extension.routes)
19
19
  .filter((routes): routes is NonNullable<typeof routes> => Boolean(routes));
20
20
 
21
- const i18nExtensions = appExtensions.filter(
22
- (extension) => extension.i18nProvider
23
- );
24
-
25
- if (i18nExtensions.length > 1) {
26
- console.warn(
27
- `Multiple app extensions provide i18nProvider (${i18nExtensions
28
- .map((extension) => extension.id)
29
- .join(", ")}). Using ${i18nExtensions[0].id}.`
30
- );
31
- }
32
-
33
- export const extensionI18nProvider = i18nExtensions[0]?.i18nProvider;
34
-
35
21
  export const extensionUserMenuItems = appExtensions
36
22
  .filter((extension) => extension.UserMenuItems)
37
23
  .map((extension) => ({
@@ -3,9 +3,12 @@
3
3
  import { Fragment, useMemo } from "react";
4
4
  import { Home } from "lucide-react";
5
5
  import {
6
+ type IResourceItem,
6
7
  matchResourceFromRoute,
7
8
  useBreadcrumb,
9
+ useGetToPath,
8
10
  useLink,
11
+ useParsed,
9
12
  useResourceParams,
10
13
  useTranslate,
11
14
  useUserFriendlyName,
@@ -24,9 +27,42 @@ export function Breadcrumb() {
24
27
  const Link = useLink();
25
28
  const { breadcrumbs } = useBreadcrumb();
26
29
  const { resources } = useResourceParams();
30
+ const { pathname } = useParsed();
31
+ const getToPath = useGetToPath();
27
32
  const translate = useTranslate();
28
33
  const getUserFriendlyName = useUserFriendlyName();
29
34
  const rootRouteResource = matchResourceFromRoute("/", resources);
35
+ const resolvedBreadcrumbs = useMemo(() => {
36
+ if (breadcrumbs.length > 0) return breadcrumbs;
37
+
38
+ const closestMatch = findClosestRouteMatch(pathname, resources);
39
+ if (!closestMatch.resource) return breadcrumbs;
40
+
41
+ const items: Array<{ label: string; href?: string }> = [
42
+ {
43
+ label:
44
+ closestMatch.resource.meta?.label ?? closestMatch.resource.name,
45
+ href: getToPath({ resource: closestMatch.resource, action: "list" }),
46
+ },
47
+ ];
48
+ const nestedAction = getNestedRouteAction(
49
+ pathname,
50
+ closestMatch.matchedSegmentCount
51
+ ) ??
52
+ (closestMatch.action && closestMatch.action !== "list"
53
+ ? closestMatch.action
54
+ : undefined);
55
+ if (nestedAction) {
56
+ items.push({
57
+ label: translate(
58
+ `buttons.${nestedAction}`,
59
+ nestedAction.charAt(0).toUpperCase() + nestedAction.slice(1)
60
+ ),
61
+ });
62
+ }
63
+
64
+ return items;
65
+ }, [breadcrumbs, getToPath, pathname, resources, translate]);
30
66
 
31
67
  const breadCrumbItems = useMemo(() => {
32
68
  const list: {
@@ -47,7 +83,7 @@ export function Breadcrumb() {
47
83
  ),
48
84
  });
49
85
 
50
- for (const { label, href } of breadcrumbs) {
86
+ for (const { label, href } of resolvedBreadcrumbs) {
51
87
  const matchingResource = resources.find((resource) => {
52
88
  const metaLabel = resource.meta?.label;
53
89
  return (
@@ -80,9 +116,9 @@ export function Breadcrumb() {
80
116
 
81
117
  return list;
82
118
  }, [
83
- breadcrumbs,
84
119
  getUserFriendlyName,
85
120
  Link,
121
+ resolvedBreadcrumbs,
86
122
  resources,
87
123
  rootRouteResource,
88
124
  translate,
@@ -114,4 +150,33 @@ export function Breadcrumb() {
114
150
  );
115
151
  }
116
152
 
153
+ function findClosestRouteMatch(
154
+ pathname: string | undefined,
155
+ resources: IResourceItem[]
156
+ ) {
157
+ const segments = pathname?.split("/").filter(Boolean) ?? [];
158
+
159
+ for (let length = segments.length; length >= 0; length -= 1) {
160
+ const route = `/${segments.slice(0, length).join("/")}`;
161
+ const match = matchResourceFromRoute(route, resources);
162
+ if (match.resource) {
163
+ return { ...match, matchedSegmentCount: length };
164
+ }
165
+ }
166
+
167
+ return { resource: undefined, matchedSegmentCount: 0 };
168
+ }
169
+
170
+ function getNestedRouteAction(
171
+ pathname: string | undefined,
172
+ matchedSegmentCount: number
173
+ ) {
174
+ const segments = pathname?.split("/").filter(Boolean) ?? [];
175
+ const action = segments[matchedSegmentCount];
176
+
177
+ return action && ["create", "edit", "show", "clone"].includes(action)
178
+ ? action
179
+ : undefined;
180
+ }
181
+
117
182
  Breadcrumb.displayName = "Breadcrumb";
@@ -218,7 +218,7 @@ function SidebarDropdownItem({
218
218
  selectedKey,
219
219
  Link,
220
220
  }: MenuItemProps & { Link: ReturnType<typeof useLink> }) {
221
- const isSelected = item.key === selectedKey;
221
+ const isSelected = isTreeItemSelected(item, selectedKey);
222
222
  const displayName = useMenuItemLabel(item);
223
223
 
224
224
  return (
@@ -239,7 +239,7 @@ function SidebarDropdownItem({
239
239
  }
240
240
 
241
241
  function SidebarItemLink({ item, selectedKey }: MenuItemProps) {
242
- const isSelected = item.key === selectedKey;
242
+ const isSelected = isTreeItemSelected(item, selectedKey);
243
243
 
244
244
  return <SidebarButton item={item} isSelected={isSelected} asLink={true} />;
245
245
  }
@@ -11,12 +11,13 @@ import { cn } from "@/lib/utils";
11
11
 
12
12
  type ListViewProps = PropsWithChildren<{
13
13
  className?: string;
14
+ resource?: string;
14
15
  }>;
15
16
 
16
- export function ListView({ children, className }: ListViewProps) {
17
+ export function ListView({ children, className, resource }: ListViewProps) {
17
18
  return (
18
19
  <div className={cn("flex flex-col", "gap-6", className)}>
19
- <ListViewHeader />
20
+ <ListViewHeader resource={resource} />
20
21
  {children}
21
22
  </div>
22
23
  );