@nocobase/portal-template-default 1.0.0 → 1.0.2

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 (194) hide show
  1. package/README.MD +90 -1
  2. package/components.json +3 -1
  3. package/package.json +21 -5
  4. package/registry/nocobase-acl/README.md +19 -0
  5. package/registry/nocobase-acl/components/acl-boundary.tsx +106 -0
  6. package/registry/nocobase-acl/components/index.ts +3 -0
  7. package/registry/nocobase-acl/components/role-options.ts +36 -0
  8. package/registry/nocobase-acl/components/role-switcher.tsx +135 -0
  9. package/registry/nocobase-acl/demo/boundary-api.tsx +186 -0
  10. package/registry/nocobase-acl/demo/components.tsx +231 -0
  11. package/registry/nocobase-acl/demo/index.tsx +505 -0
  12. package/registry/nocobase-acl/demo/lazy-route.tsx +10 -0
  13. package/registry/nocobase-acl/demo/prompt-generator.tsx +134 -0
  14. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  15. package/registry/nocobase-acl/demo/scenario-section.tsx +37 -0
  16. package/registry/nocobase-acl/extension.tsx +71 -0
  17. package/registry/nocobase-acl/index.ts +1 -0
  18. package/registry/nocobase-acl/tests/acl-regression.mjs +314 -0
  19. package/registry/nocobase-ai/README.md +70 -0
  20. package/registry/nocobase-ai/adapters/react-hook-form.ts +20 -0
  21. package/registry/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  22. package/registry/nocobase-ai/components/ai-root-provider.tsx +32 -0
  23. package/registry/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  24. package/registry/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  25. package/registry/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  26. package/registry/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  27. package/registry/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  28. package/registry/nocobase-ai/components/chat/chat-header.tsx +88 -0
  29. package/registry/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  30. package/registry/nocobase-ai/components/chat/chat-message.tsx +252 -0
  31. package/registry/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  32. package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
  33. package/registry/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  34. package/registry/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  35. package/registry/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  36. package/registry/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  37. package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  38. package/registry/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  39. package/registry/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  40. package/registry/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  41. package/registry/nocobase-ai/components/index.ts +59 -0
  42. package/registry/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  43. package/registry/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  44. package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  45. package/registry/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  46. package/registry/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  47. package/registry/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  48. package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  49. package/registry/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  50. package/registry/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  51. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  52. package/registry/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  53. package/registry/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  54. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  55. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  56. package/registry/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  57. package/registry/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  58. package/registry/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  59. package/registry/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  60. package/registry/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  61. package/registry/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  62. package/registry/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  63. package/registry/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  64. package/registry/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  65. package/registry/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  66. package/registry/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  67. package/registry/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  68. package/registry/nocobase-ai/demo/configuration-gate.tsx +49 -0
  69. package/registry/nocobase-ai/demo/container-showcase.tsx +210 -0
  70. package/registry/nocobase-ai/demo/floating.tsx +134 -0
  71. package/registry/nocobase-ai/demo/index.tsx +346 -0
  72. package/registry/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  73. package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  74. package/registry/nocobase-ai/demo/page-context.tsx +884 -0
  75. package/registry/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  76. package/registry/nocobase-ai/demo/prompt-card.tsx +41 -0
  77. package/registry/nocobase-ai/demo/prompt-generator.tsx +452 -0
  78. package/registry/nocobase-ai/demo/shortcut.tsx +1207 -0
  79. package/registry/nocobase-ai/demo/tool-cards.tsx +530 -0
  80. package/registry/nocobase-ai/extension.tsx +158 -0
  81. package/registry/nocobase-ai/global-ai-chat.tsx +213 -0
  82. package/registry/nocobase-ai/index.ts +3 -0
  83. package/registry/nocobase-ai/providers/ai-provider.tsx +323 -0
  84. package/registry/nocobase-ai/providers/avatars.ts +817 -0
  85. package/registry/nocobase-ai/providers/chat-context.tsx +109 -0
  86. package/registry/nocobase-ai/providers/chat-controller.ts +84 -0
  87. package/registry/nocobase-ai/providers/chat-message-utils.ts +198 -0
  88. package/registry/nocobase-ai/providers/chat-provider.tsx +871 -0
  89. package/registry/nocobase-ai/providers/chat-reducer.ts +147 -0
  90. package/registry/nocobase-ai/providers/chat-task-utils.ts +105 -0
  91. package/registry/nocobase-ai/providers/chat-transport.ts +184 -0
  92. package/registry/nocobase-ai/providers/form-registry.tsx +271 -0
  93. package/registry/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  94. package/registry/nocobase-ai/providers/index.ts +61 -0
  95. package/registry/nocobase-ai/providers/model.ts +32 -0
  96. package/registry/nocobase-ai/providers/page-context.tsx +136 -0
  97. package/registry/nocobase-ai/providers/stream-coalescer.ts +60 -0
  98. package/registry/nocobase-ai/providers/stream-event-utils.ts +82 -0
  99. package/registry/nocobase-ai/providers/stream-parser.ts +61 -0
  100. package/registry/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  101. package/registry/nocobase-ai/providers/types.ts +183 -0
  102. package/registry/nocobase-ai/providers/ui-message-stream.ts +463 -0
  103. package/registry/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  104. package/registry/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  105. package/registry/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  106. package/registry/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  107. package/registry/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  108. package/registry/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  109. package/registry/nocobase-ai/providers/use-conversation-history.ts +178 -0
  110. package/registry/nocobase-ai/services/index.ts +7 -0
  111. package/registry/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  112. package/registry/nocobase-ai/services/types.ts +82 -0
  113. package/registry/nocobase-ai/tests/business-report-regression.mjs +70 -0
  114. package/registry/nocobase-ai/tests/form-filler-regression.mjs +129 -0
  115. package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +128 -0
  116. package/registry/nocobase-ai/tests/page-context-regression.mjs +48 -0
  117. package/registry/nocobase-ai/tests/stream-regression.mjs +207 -0
  118. package/registry/nocobase-i18n/README.md +53 -0
  119. package/registry/nocobase-i18n/components/index.ts +1 -0
  120. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  121. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  122. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  123. package/registry/nocobase-i18n/extension.tsx +46 -0
  124. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  125. package/registry/nocobase-i18n/index.ts +6 -0
  126. package/registry/nocobase-i18n/locale-store.ts +134 -0
  127. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  128. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  129. package/registry/nocobase-i18n/provider.tsx +32 -0
  130. package/registry/nocobase-i18n/runtime.ts +118 -0
  131. package/registry/nocobase-i18n/server-resources.ts +184 -0
  132. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  133. package/registry.config.json +163 -0
  134. package/scripts/registry.mjs +164 -0
  135. package/src/App.css +95 -0
  136. package/src/App.tsx +87 -69
  137. package/src/app/extension.ts +11 -0
  138. package/src/app/extensions.tsx +49 -0
  139. package/src/components/access-control/access-denied.tsx +30 -0
  140. package/src/components/access-control/acl-bootstrap.tsx +9 -0
  141. package/src/components/access-control/acl-gate.tsx +36 -0
  142. package/src/components/access-control/navigate-to-accessible-resource.tsx +17 -0
  143. package/src/components/access-control/resource-access-guard.tsx +21 -0
  144. package/src/components/access-control/use-acl-runtime.ts +28 -0
  145. package/src/components/app-shell/breadcrumb.tsx +39 -3
  146. package/src/components/app-shell/document-title-handler.tsx +11 -20
  147. package/src/components/app-shell/header.tsx +22 -4
  148. package/src/components/app-shell/loading-overlay.tsx +2 -4
  149. package/src/components/app-shell/loading-state.tsx +15 -0
  150. package/src/components/app-shell/sidebar.tsx +103 -45
  151. package/src/components/data-table/data-table-pagination.tsx +27 -7
  152. package/src/components/data-table/data-table-styles.ts +57 -0
  153. package/src/components/data-table/data-table.tsx +14 -59
  154. package/src/components/resources/resource-label.ts +101 -0
  155. package/src/components/resources/views/create-view.tsx +17 -14
  156. package/src/components/resources/views/edit-view.tsx +17 -15
  157. package/src/components/resources/views/list-view.tsx +20 -10
  158. package/src/components/resources/views/show-view.tsx +20 -18
  159. package/src/index.tsx +2 -3
  160. package/src/lib/i18n.ts +161 -0
  161. package/src/lib/nocobase/acl/action.ts +147 -0
  162. package/src/lib/nocobase/acl/data-source.ts +24 -0
  163. package/src/lib/nocobase/acl/index.ts +6 -0
  164. package/src/lib/nocobase/acl/menu.ts +41 -0
  165. package/src/lib/nocobase/acl/record-permissions.ts +111 -0
  166. package/src/lib/nocobase/acl/store.ts +182 -0
  167. package/src/lib/nocobase/acl/types.ts +96 -0
  168. package/src/lib/nocobase/client.ts +279 -0
  169. package/src/lib/nocobase/error.ts +32 -0
  170. package/src/locales/en-US.ts +82 -0
  171. package/src/locales/index.ts +13 -0
  172. package/src/locales/zh-CN.ts +79 -0
  173. package/src/pages/users/create.tsx +98 -0
  174. package/src/pages/users/edit.tsx +92 -0
  175. package/src/pages/users/form-context.ts +57 -0
  176. package/src/pages/users/form-fields.tsx +211 -0
  177. package/src/pages/users/list.tsx +205 -0
  178. package/src/pages/users/show.tsx +187 -0
  179. package/src/pages/users/types.ts +20 -0
  180. package/src/providers/access-control.ts +34 -0
  181. package/src/providers/auth.ts +74 -60
  182. package/src/providers/constants.ts +3 -37
  183. package/src/providers/data.ts +242 -85
  184. package/vite.config.ts +17 -0
  185. package/src/pages/blog-posts/create.tsx +0 -177
  186. package/src/pages/blog-posts/edit.tsx +0 -197
  187. package/src/pages/blog-posts/list.tsx +0 -131
  188. package/src/pages/blog-posts/show.tsx +0 -81
  189. package/src/pages/categories/create.tsx +0 -73
  190. package/src/pages/categories/edit.tsx +0 -73
  191. package/src/pages/categories/index.ts +0 -4
  192. package/src/pages/categories/list.tsx +0 -86
  193. package/src/pages/categories/show.tsx +0 -37
  194. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -7,6 +7,8 @@ import {
7
7
  useBreadcrumb,
8
8
  useLink,
9
9
  useResourceParams,
10
+ useTranslate,
11
+ useUserFriendlyName,
10
12
  } from "@refinedev/core";
11
13
  import {
12
14
  BreadcrumbSeparator as ShadcnBreadcrumbSeparator,
@@ -15,11 +17,15 @@ import {
15
17
  BreadcrumbPage as ShadcnBreadcrumbPage,
16
18
  Breadcrumb as ShadcnBreadcrumb,
17
19
  } from "@/components/ui/breadcrumb";
20
+ import { resolveTranslatableText } from "@/lib/i18n";
21
+ import { getResourceLabel } from "@/components/resources/resource-label";
18
22
 
19
23
  export function Breadcrumb() {
20
24
  const Link = useLink();
21
25
  const { breadcrumbs } = useBreadcrumb();
22
26
  const { resources } = useResourceParams();
27
+ const translate = useTranslate();
28
+ const getUserFriendlyName = useUserFriendlyName();
23
29
  const rootRouteResource = matchResourceFromRoute("/", resources);
24
30
 
25
31
  const breadCrumbItems = useMemo(() => {
@@ -42,15 +48,45 @@ export function Breadcrumb() {
42
48
  });
43
49
 
44
50
  for (const { label, href } of breadcrumbs) {
51
+ const matchingResource = resources.find((resource) => {
52
+ const metaLabel = resource.meta?.label;
53
+ return (
54
+ label === metaLabel ||
55
+ label === resource.name ||
56
+ (href &&
57
+ [resource.list, resource.create, resource.edit, resource.show].some(
58
+ (route) => typeof route === "string" && route === href
59
+ ))
60
+ );
61
+ });
62
+ const displayLabel = matchingResource
63
+ ? getResourceLabel(
64
+ matchingResource,
65
+ "plural",
66
+ translate,
67
+ getUserFriendlyName
68
+ )
69
+ : resolveTranslatableText(label);
45
70
  list.push({
46
- key: `breadcrumb-item-${label}`,
71
+ key: `breadcrumb-item-${displayLabel}`,
47
72
  href: href ?? "",
48
- Component: href ? <Link to={href}>{label}</Link> : <span>{label}</span>,
73
+ Component: href ? (
74
+ <Link to={href}>{displayLabel}</Link>
75
+ ) : (
76
+ <span>{displayLabel}</span>
77
+ ),
49
78
  });
50
79
  }
51
80
 
52
81
  return list;
53
- }, [breadcrumbs, Link, rootRouteResource]);
82
+ }, [
83
+ breadcrumbs,
84
+ getUserFriendlyName,
85
+ Link,
86
+ resources,
87
+ rootRouteResource,
88
+ translate,
89
+ ]);
54
90
 
55
91
  return (
56
92
  <ShadcnBreadcrumb>
@@ -1,19 +1,14 @@
1
1
  import type { Action, IResourceItem } from "@refinedev/core";
2
- import {
3
- useParsed,
4
- useTranslate,
5
- useUserFriendlyName,
6
- } from "@refinedev/core";
2
+ import { useParsed, useTranslate } from "@refinedev/core";
7
3
  import { useLayoutEffect } from "react";
8
4
  import { useLocation } from "react-router";
5
+ import { useResourceLabel } from "@/components/resources/resource-label";
9
6
 
10
7
  export type ActionPrefixContext = {
11
8
  id?: string;
12
9
  };
13
10
 
14
- export type ActionPrefix =
15
- | string
16
- | ((context: ActionPrefixContext) => string);
11
+ export type ActionPrefix = string | ((context: ActionPrefixContext) => string);
17
12
 
18
13
  export type DocumentTitleContext = {
19
14
  action?: Action;
@@ -85,19 +80,14 @@ export function DocumentTitleHandler({
85
80
  const location = useLocation();
86
81
  const { action, id, params, pathname, resource } = useParsed();
87
82
  const translate = useTranslate();
88
- const getUserFriendlyName = useUserFriendlyName();
83
+ const identifier = resource?.identifier ?? resource?.name;
84
+ const resourceNameFallback = useResourceLabel(
85
+ resource,
86
+ action === "list" ? "plural" : "singular",
87
+ identifier
88
+ );
89
89
 
90
90
  useLayoutEffect(() => {
91
- const identifier = resource?.identifier ?? resource?.name;
92
- const resourceNameFallback =
93
- resource?.meta?.label ??
94
- (identifier
95
- ? getUserFriendlyName(
96
- identifier,
97
- action === "list" ? "plural" : "singular"
98
- )
99
- : undefined);
100
-
101
91
  const resourceName = resource?.name
102
92
  ? safeTranslate(
103
93
  translate,
@@ -155,12 +145,13 @@ export function DocumentTitleHandler({
155
145
  appName,
156
146
  defaultTitle,
157
147
  formatTitle,
158
- getUserFriendlyName,
159
148
  id,
149
+ identifier,
160
150
  location,
161
151
  params,
162
152
  pathname,
163
153
  resource,
154
+ resourceNameFallback,
164
155
  separator,
165
156
  translate,
166
157
  ]);
@@ -1,4 +1,8 @@
1
- import { useActiveAuthProvider, useLogout } from "@refinedev/core";
1
+ import {
2
+ useActiveAuthProvider,
3
+ useLogout,
4
+ useTranslate,
5
+ } from "@refinedev/core";
2
6
  import {
3
7
  DropdownMenu,
4
8
  DropdownMenuItem,
@@ -13,6 +17,7 @@ import { useSidebar, SidebarTrigger } from "@/components/ui/sidebar";
13
17
  import { LogOutIcon } from "lucide-react";
14
18
  import { cn } from "@/lib/utils";
15
19
  import { Brand } from "@/components/app-shell/brand";
20
+ import { extensionUserMenuItems } from "@/app/extensions";
16
21
 
17
22
  export const Header = () => {
18
23
  const { isMobile } = useSidebar();
@@ -21,6 +26,8 @@ export const Header = () => {
21
26
  };
22
27
 
23
28
  function DesktopHeader() {
29
+ const translate = useTranslate();
30
+
24
31
  return (
25
32
  <header
26
33
  className={cn(
@@ -44,7 +51,7 @@ function DesktopHeader() {
44
51
  <SidebarTrigger className="size-9 rounded-xl text-muted-foreground hover:text-foreground" />
45
52
  <div className="hidden h-5 w-px bg-border sm:block" />
46
53
  <span className="hidden text-sm font-medium text-muted-foreground sm:block">
47
- AI application workspace
54
+ {translate("shell.workspace", "AI application workspace")}
48
55
  </span>
49
56
  </div>
50
57
  <div className="flex items-center gap-2">
@@ -84,13 +91,17 @@ function MobileHeader() {
84
91
  )}
85
92
  />
86
93
  <Brand logoClassName="h-6" />
87
- <ThemeToggle className={cn("size-9")} />
94
+ <div className="flex shrink-0 items-center gap-1">
95
+ <ThemeToggle className="size-9" />
96
+ <UserDropdown />
97
+ </div>
88
98
  </header>
89
99
  );
90
100
  }
91
101
 
92
102
  const UserDropdown = () => {
93
103
  const { mutate: logout, isPending: isLoggingOut } = useLogout();
104
+ const translate = useTranslate();
94
105
 
95
106
  const authProvider = useActiveAuthProvider();
96
107
 
@@ -107,6 +118,9 @@ const UserDropdown = () => {
107
118
  <div className="px-2 py-2">
108
119
  <UserInfo />
109
120
  </div>
121
+ {extensionUserMenuItems.map(({ id, Component }) => (
122
+ <Component key={id} />
123
+ ))}
110
124
  <DropdownMenuSeparator />
111
125
  <DropdownMenuItem
112
126
  className="mt-1 min-h-9 cursor-pointer gap-2 px-2 text-muted-foreground focus:text-foreground"
@@ -115,7 +129,11 @@ const UserDropdown = () => {
115
129
  }}
116
130
  >
117
131
  <LogOutIcon />
118
- <span>{isLoggingOut ? "Signing out..." : "Sign out"}</span>
132
+ <span>
133
+ {isLoggingOut
134
+ ? translate("auth.signingOut", "Signing out...")
135
+ : translate("auth.signOut", "Sign out")}
136
+ </span>
119
137
  </DropdownMenuItem>
120
138
  </DropdownMenuContent>
121
139
  </DropdownMenu>
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
- import { Loader2 } from "lucide-react";
5
4
  import { cn } from "@/lib/utils";
5
+ import { LoadingState } from "@/components/app-shell/loading-state";
6
6
 
7
7
  interface LoadingOverlayProps extends React.HTMLAttributes<HTMLDivElement> {
8
8
  loading?: boolean;
@@ -25,9 +25,7 @@ export const LoadingOverlay = React.forwardRef<
25
25
  className
26
26
  )}
27
27
  >
28
- <div className="flex flex-col items-center gap-2">
29
- <Loader2 className="h-8 w-8 animate-spin text-primary" />
30
- </div>
28
+ <LoadingState />
31
29
  </div>
32
30
  </div>
33
31
  );
@@ -0,0 +1,15 @@
1
+ import { Loader2 } from "lucide-react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ export function LoadingState({ className }: { className?: string }) {
6
+ return (
7
+ <div
8
+ role="status"
9
+ className={cn("flex items-center justify-center", className)}
10
+ >
11
+ <Loader2 className="size-7 animate-spin text-primary" />
12
+ <span className="sr-only">Loading</span>
13
+ </div>
14
+ );
15
+ }
@@ -4,6 +4,8 @@ import React from "react";
4
4
  import {
5
5
  useMenu,
6
6
  useLink,
7
+ useTranslate,
8
+ useUserFriendlyName,
7
9
  type TreeMenuItem,
8
10
  } from "@refinedev/core";
9
11
  import {
@@ -29,10 +31,17 @@ import { Button } from "@/components/ui/button";
29
31
  import { ChevronRight, ListIcon, ShieldCheck } from "lucide-react";
30
32
  import { cn } from "@/lib/utils";
31
33
  import { Brand } from "@/components/app-shell/brand";
34
+ import { filterMenuItemsByAcl, useAclSnapshot } from "@/lib/nocobase/acl";
35
+ import { getResourceLabel } from "@/components/resources/resource-label";
32
36
 
33
37
  export function Sidebar() {
34
38
  const { open } = useShadcnSidebar();
35
39
  const { menuItems, selectedKey } = useMenu();
40
+ const acl = useAclSnapshot();
41
+ const allowedMenuItems = React.useMemo(
42
+ () => filterMenuItemsByAcl(menuItems, acl),
43
+ [acl, menuItems]
44
+ );
36
45
 
37
46
  return (
38
47
  <ShadcnSidebar
@@ -55,7 +64,7 @@ export function Sidebar() {
55
64
  }
56
65
  )}
57
66
  >
58
- {menuItems.map((item: TreeMenuItem) => (
67
+ {allowedMenuItems.map((item: TreeMenuItem) => (
59
68
  <SidebarItem
60
69
  key={item.key || item.name}
61
70
  item={item}
@@ -93,6 +102,7 @@ function SidebarItem({ item, selectedKey }: MenuItemProps) {
93
102
  function SidebarItemGroup({ item, selectedKey }: MenuItemProps) {
94
103
  const { children } = item;
95
104
  const { open } = useShadcnSidebar();
105
+ const displayName = useMenuItemLabel(item);
96
106
 
97
107
  return (
98
108
  <div className={cn("mt-2 border-t", "border-sidebar-border/70", "pt-4")}>
@@ -116,7 +126,7 @@ function SidebarItemGroup({ item, selectedKey }: MenuItemProps) {
116
126
  }
117
127
  )}
118
128
  >
119
- {getDisplayName(item)}
129
+ {displayName}
120
130
  </span>
121
131
  {children && children.length > 0 && (
122
132
  <div className={cn("flex", "flex-col")}>
@@ -135,6 +145,7 @@ function SidebarItemGroup({ item, selectedKey }: MenuItemProps) {
135
145
 
136
146
  function SidebarItemCollapsible({ item, selectedKey }: MenuItemProps) {
137
147
  const { name, children } = item;
148
+ const isSelected = isTreeItemSelected(item, selectedKey);
138
149
 
139
150
  const chevronIcon = (
140
151
  <ChevronRight
@@ -151,9 +162,19 @@ function SidebarItemCollapsible({ item, selectedKey }: MenuItemProps) {
151
162
  );
152
163
 
153
164
  return (
154
- <Collapsible key={`collapsible-${name}`} className={cn("w-full", "group")}>
165
+ <Collapsible
166
+ key={`collapsible-${name}`}
167
+ defaultOpen={isSelected}
168
+ className={cn("w-full", "group")}
169
+ >
155
170
  <CollapsibleTrigger
156
- render={<SidebarButton item={item} rightIcon={chevronIcon} />}
171
+ render={
172
+ <SidebarButton
173
+ item={item}
174
+ rightIcon={chevronIcon}
175
+ isSelected={isSelected}
176
+ />
177
+ }
157
178
  />
158
179
  <CollapsibleContent className={cn("ml-6", "flex", "flex-col", "gap-2")}>
159
180
  {children?.map((child: TreeMenuItem) => (
@@ -171,38 +192,52 @@ function SidebarItemCollapsible({ item, selectedKey }: MenuItemProps) {
171
192
  function SidebarItemDropdown({ item, selectedKey }: MenuItemProps) {
172
193
  const { children } = item;
173
194
  const Link = useLink();
195
+ const isSelected = isTreeItemSelected(item, selectedKey);
174
196
 
175
197
  return (
176
198
  <DropdownMenu>
177
- <DropdownMenuTrigger render={<SidebarButton item={item} />} />
199
+ <DropdownMenuTrigger
200
+ render={<SidebarButton item={item} isSelected={isSelected} />}
201
+ />
178
202
  <DropdownMenuContent side="right" align="start">
179
- {children?.map((child: TreeMenuItem) => {
180
- const { key: childKey } = child;
181
- const isSelected = childKey === selectedKey;
182
-
183
- return (
184
- <DropdownMenuItem
185
- key={childKey || child.name}
186
- render={<Link
187
- to={child.route || ""}
188
- className={cn("flex w-full items-center gap-2", {
189
- "bg-accent text-accent-foreground": isSelected,
190
- })}
191
- />}
192
- >
193
- <ItemIcon
194
- icon={child.meta?.icon ?? child.icon}
195
- isSelected={isSelected}
196
- />
197
- <span>{getDisplayName(child)}</span>
198
- </DropdownMenuItem>
199
- );
200
- })}
203
+ {children?.map((child: TreeMenuItem) => (
204
+ <SidebarDropdownItem
205
+ key={child.key || child.name}
206
+ item={child}
207
+ selectedKey={selectedKey}
208
+ Link={Link}
209
+ />
210
+ ))}
201
211
  </DropdownMenuContent>
202
212
  </DropdownMenu>
203
213
  );
204
214
  }
205
215
 
216
+ function SidebarDropdownItem({
217
+ item,
218
+ selectedKey,
219
+ Link,
220
+ }: MenuItemProps & { Link: ReturnType<typeof useLink> }) {
221
+ const isSelected = item.key === selectedKey;
222
+ const displayName = useMenuItemLabel(item);
223
+
224
+ return (
225
+ <DropdownMenuItem
226
+ render={
227
+ <Link
228
+ to={item.route || ""}
229
+ className={cn("flex w-full items-center gap-2", {
230
+ "bg-accent text-accent-foreground": isSelected,
231
+ })}
232
+ />
233
+ }
234
+ >
235
+ <ItemIcon icon={item.meta?.icon ?? item.icon} isSelected={isSelected} />
236
+ <span>{displayName}</span>
237
+ </DropdownMenuItem>
238
+ );
239
+ }
240
+
206
241
  function SidebarItemLink({ item, selectedKey }: MenuItemProps) {
207
242
  const isSelected = item.key === selectedKey;
208
243
 
@@ -227,7 +262,10 @@ function SidebarHeader() {
227
262
  >
228
263
  <Brand
229
264
  showText={open}
230
- logoClassName={cn("transition-transform duration-200", !open && "size-9")}
265
+ logoClassName={cn(
266
+ "transition-transform duration-200",
267
+ !open && "size-9"
268
+ )}
231
269
  />
232
270
  </ShadcnSidebarHeader>
233
271
  );
@@ -235,6 +273,7 @@ function SidebarHeader() {
235
273
 
236
274
  function SidebarFooter() {
237
275
  const { open } = useShadcnSidebar();
276
+ const translate = useTranslate();
238
277
 
239
278
  return (
240
279
  <ShadcnSidebarFooter className="border-t border-sidebar-border/70 p-0">
@@ -248,10 +287,13 @@ function SidebarFooter() {
248
287
  {open && (
249
288
  <div className="min-w-0 text-xs leading-4">
250
289
  <div className="font-semibold text-sidebar-foreground">
251
- AI builds freely.
290
+ {translate("shell.footer.freedom", "AI builds freely.")}
252
291
  </div>
253
292
  <div className="text-muted-foreground">
254
- NocoBase keeps it reliable.
293
+ {translate(
294
+ "shell.footer.reliability",
295
+ "NocoBase keeps it reliable."
296
+ )}
255
297
  </div>
256
298
  </div>
257
299
  )}
@@ -260,8 +302,23 @@ function SidebarFooter() {
260
302
  );
261
303
  }
262
304
 
263
- function getDisplayName(item: TreeMenuItem) {
264
- return item.meta?.label ?? item.label ?? item.name;
305
+ function useMenuItemLabel(item: TreeMenuItem) {
306
+ const translate = useTranslate();
307
+ const getUserFriendlyName = useUserFriendlyName();
308
+
309
+ return getResourceLabel(
310
+ item,
311
+ "plural",
312
+ translate,
313
+ getUserFriendlyName,
314
+ item.name
315
+ );
316
+ }
317
+
318
+ function isTreeItemSelected(item: TreeMenuItem, selectedKey?: string) {
319
+ return (
320
+ item.key === selectedKey || Boolean(selectedKey?.startsWith(`${item.key}/`))
321
+ );
265
322
  }
266
323
 
267
324
  type IconProps = {
@@ -300,24 +357,22 @@ function SidebarButton({
300
357
  ...props
301
358
  }: SidebarButtonProps) {
302
359
  const Link = useLink();
360
+ const displayName = useMenuItemLabel(item);
303
361
 
304
362
  const buttonContent = (
305
363
  <>
306
364
  <ItemIcon icon={item.meta?.icon ?? item.icon} isSelected={isSelected} />
307
365
  <span
308
- className={cn(
309
- "tracking-[-0.00875rem] text-foreground",
310
- {
311
- "flex-1": rightIcon,
312
- "text-left": rightIcon,
313
- "line-clamp-1": !rightIcon,
314
- truncate: !rightIcon,
315
- "font-normal": !isSelected,
316
- "font-medium": isSelected,
317
- },
318
- )}
366
+ className={cn("tracking-[-0.00875rem] text-foreground", {
367
+ "flex-1": rightIcon,
368
+ "text-left": rightIcon,
369
+ "line-clamp-1": !rightIcon,
370
+ truncate: !rightIcon,
371
+ "font-normal": !isSelected,
372
+ "font-medium": isSelected,
373
+ })}
319
374
  >
320
- {getDisplayName(item)}
375
+ {displayName}
321
376
  </span>
322
377
  {rightIcon}
323
378
  </>
@@ -327,7 +382,10 @@ function SidebarButton({
327
382
  <Button
328
383
  render={
329
384
  asLink && item.route ? (
330
- <Link to={item.route} className={cn("flex w-full items-center gap-2")} />
385
+ <Link
386
+ to={item.route}
387
+ className={cn("flex w-full items-center gap-2")}
388
+ />
331
389
  ) : undefined
332
390
  }
333
391
  variant="ghost"
@@ -7,6 +7,7 @@ import {
7
7
  ChevronsRight,
8
8
  } from "lucide-react";
9
9
  import { useMemo } from "react";
10
+ import { useTranslate } from "@refinedev/core";
10
11
 
11
12
  import {
12
13
  Select,
@@ -35,6 +36,7 @@ export function DataTablePagination({
35
36
  setPageSize,
36
37
  total,
37
38
  }: DataTablePaginationProps) {
39
+ const translate = useTranslate();
38
40
  const pageSizeOptions = useMemo(() => {
39
41
  const baseOptions = [10, 20, 30, 40, 50];
40
42
  const optionsSet = new Set(baseOptions);
@@ -66,11 +68,19 @@ export function DataTablePagination({
66
68
  "whitespace-nowrap"
67
69
  )}
68
70
  >
69
- {typeof total === "number" ? `${total} row(s)` : null}
71
+ {typeof total === "number"
72
+ ? translate(
73
+ "table.pagination.total",
74
+ { count: total },
75
+ `${total} row(s)`
76
+ )
77
+ : null}
70
78
  </div>
71
79
  <div className={cn("flex", "items-center", "flex-wrap", "gap-2")}>
72
80
  <div className={cn("flex", "items-center", "gap-2")}>
73
- <span className={cn("text-sm", "font-medium")}>Rows per page</span>
81
+ <span className={cn("text-sm", "font-medium")}>
82
+ {translate("table.pagination.pageSize", "Rows per page")}
83
+ </span>
74
84
  <Select
75
85
  value={`${pageSize}`}
76
86
  onValueChange={(v) => setPageSize(Number(v))}
@@ -97,7 +107,11 @@ export function DataTablePagination({
97
107
  "font-medium"
98
108
  )}
99
109
  >
100
- Page {currentPage} of {pageCount}
110
+ {translate(
111
+ "table.pagination.page",
112
+ { current: currentPage, total: pageCount },
113
+ `Page ${currentPage} of ${pageCount}`
114
+ )}
101
115
  </div>
102
116
  <div className={cn("flex", "items-center", "gap-2")}>
103
117
  <Button
@@ -105,7 +119,10 @@ export function DataTablePagination({
105
119
  className={cn("hidden", "h-8", "w-8", "p-0", "lg:flex")}
106
120
  onClick={() => setCurrentPage(1)}
107
121
  disabled={currentPage === 1}
108
- aria-label="Go to first page"
122
+ aria-label={translate(
123
+ "table.pagination.first",
124
+ "Go to first page"
125
+ )}
109
126
  >
110
127
  <ChevronsLeft />
111
128
  </Button>
@@ -114,7 +131,10 @@ export function DataTablePagination({
114
131
  className={cn("h-8", "w-8", "p-0")}
115
132
  onClick={() => setCurrentPage(currentPage - 1)}
116
133
  disabled={currentPage === 1}
117
- aria-label="Go to previous page"
134
+ aria-label={translate(
135
+ "table.pagination.previous",
136
+ "Go to previous page"
137
+ )}
118
138
  >
119
139
  <ChevronLeft />
120
140
  </Button>
@@ -123,7 +143,7 @@ export function DataTablePagination({
123
143
  className={cn("h-8", "w-8", "p-0")}
124
144
  onClick={() => setCurrentPage(currentPage + 1)}
125
145
  disabled={currentPage === pageCount}
126
- aria-label="Go to next page"
146
+ aria-label={translate("table.pagination.next", "Go to next page")}
127
147
  >
128
148
  <ChevronRight />
129
149
  </Button>
@@ -132,7 +152,7 @@ export function DataTablePagination({
132
152
  className={cn("hidden", "h-8", "w-8", "p-0", "lg:flex")}
133
153
  onClick={() => setCurrentPage(pageCount)}
134
154
  disabled={currentPage === pageCount}
135
- aria-label="Go to last page"
155
+ aria-label={translate("table.pagination.last", "Go to last page")}
136
156
  >
137
157
  <ChevronsRight />
138
158
  </Button>
@@ -0,0 +1,57 @@
1
+ import type { Column } from "@tanstack/react-table";
2
+ import type { CSSProperties } from "react";
3
+
4
+ export function getCommonStyles<TData>({
5
+ column,
6
+ isOverflowing,
7
+ }: {
8
+ column: Column<TData>;
9
+ isOverflowing: {
10
+ horizontal: boolean;
11
+ vertical: boolean;
12
+ };
13
+ }): CSSProperties {
14
+ const isPinned = column.getIsPinned();
15
+ const isLastLeftPinnedColumn =
16
+ isPinned === "left" && column.getIsLastColumn("left");
17
+ const isFirstRightPinnedColumn =
18
+ isPinned === "right" && column.getIsFirstColumn("right");
19
+
20
+ return {
21
+ boxShadow:
22
+ isOverflowing.horizontal && isLastLeftPinnedColumn
23
+ ? "-4px 0 4px -4px var(--border) inset"
24
+ : isOverflowing.horizontal && isFirstRightPinnedColumn
25
+ ? "4px 0 4px -4px var(--border) inset"
26
+ : undefined,
27
+ left:
28
+ isOverflowing.horizontal && isPinned === "left"
29
+ ? `${column.getStart("left")}px`
30
+ : undefined,
31
+ right:
32
+ isOverflowing.horizontal && isPinned === "right"
33
+ ? `${column.getAfter("right")}px`
34
+ : undefined,
35
+ opacity: 1,
36
+ position: isOverflowing.horizontal && isPinned ? "sticky" : "relative",
37
+ background: isOverflowing.horizontal && isPinned ? "var(--background)" : "",
38
+ borderTopRightRadius:
39
+ isOverflowing.horizontal && isPinned === "right"
40
+ ? "var(--radius)"
41
+ : undefined,
42
+ borderBottomRightRadius:
43
+ isOverflowing.horizontal && isPinned === "right"
44
+ ? "var(--radius)"
45
+ : undefined,
46
+ borderTopLeftRadius:
47
+ isOverflowing.horizontal && isPinned === "left"
48
+ ? "var(--radius)"
49
+ : undefined,
50
+ borderBottomLeftRadius:
51
+ isOverflowing.horizontal && isPinned === "left"
52
+ ? "var(--radius)"
53
+ : undefined,
54
+ width: column.getSize(),
55
+ zIndex: isOverflowing.horizontal && isPinned ? 1 : 0,
56
+ };
57
+ }