@nocobase/portal-template-default 1.0.1 → 1.0.3

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 (186) hide show
  1. package/README.MD +22 -3
  2. package/package.json +4 -2
  3. package/registry/nocobase-acl/components/role-options.ts +7 -0
  4. package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
  5. package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
  6. package/registry/nocobase-acl/demo/components.tsx +32 -11
  7. package/registry/nocobase-acl/demo/index.tsx +45 -14
  8. package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
  9. package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
  10. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
  11. package/registry/nocobase-acl/extension.tsx +1 -1
  12. package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
  13. package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
  14. package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
  15. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
  16. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
  17. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
  18. package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
  19. package/registry/nocobase-ai/extension.tsx +2 -7
  20. package/registry/nocobase-i18n/README.md +53 -0
  21. package/registry/nocobase-i18n/components/index.ts +1 -0
  22. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  23. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  24. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  25. package/registry/nocobase-i18n/extension.tsx +46 -0
  26. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  27. package/registry/nocobase-i18n/index.ts +6 -0
  28. package/registry/nocobase-i18n/locale-store.ts +134 -0
  29. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  30. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  31. package/registry/nocobase-i18n/provider.tsx +32 -0
  32. package/registry/nocobase-i18n/runtime.ts +118 -0
  33. package/registry/nocobase-i18n/server-resources.ts +184 -0
  34. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  35. package/registry.config.json +34 -14
  36. package/scripts/registry.mjs +5 -0
  37. package/src/App.tsx +27 -86
  38. package/src/app/extension.ts +3 -1
  39. package/src/app/extensions.tsx +21 -0
  40. package/src/components/access-control/acl-gate.tsx +4 -10
  41. package/src/components/app-shell/breadcrumb.tsx +39 -3
  42. package/src/components/app-shell/document-title-handler.tsx +11 -20
  43. package/src/components/app-shell/header.tsx +18 -3
  44. package/src/components/app-shell/loading-overlay.tsx +2 -4
  45. package/src/components/app-shell/loading-state.tsx +15 -0
  46. package/src/components/app-shell/sidebar.tsx +75 -47
  47. package/src/components/data-table/data-table-pagination.tsx +27 -7
  48. package/src/components/data-table/data-table-styles.ts +57 -0
  49. package/src/components/data-table/data-table.tsx +14 -59
  50. package/src/components/resources/resource-label.ts +101 -0
  51. package/src/components/resources/views/create-view.tsx +17 -14
  52. package/src/components/resources/views/edit-view.tsx +17 -15
  53. package/src/components/resources/views/list-view.tsx +20 -10
  54. package/src/components/resources/views/show-view.tsx +20 -18
  55. package/src/extensions/nocobase-acl/README.md +19 -0
  56. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +106 -0
  57. package/src/extensions/nocobase-acl/components/index.ts +3 -0
  58. package/src/extensions/nocobase-acl/components/role-options.ts +36 -0
  59. package/src/extensions/nocobase-acl/components/role-switcher.tsx +135 -0
  60. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +186 -0
  61. package/src/extensions/nocobase-acl/demo/components.tsx +231 -0
  62. package/src/extensions/nocobase-acl/demo/index.tsx +505 -0
  63. package/src/extensions/nocobase-acl/demo/lazy-route.tsx +10 -0
  64. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +134 -0
  65. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  66. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +37 -0
  67. package/src/extensions/nocobase-acl/extension.tsx +71 -0
  68. package/src/extensions/nocobase-acl/index.ts +1 -0
  69. package/src/extensions/nocobase-ai/README.md +70 -0
  70. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +20 -0
  71. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  72. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +32 -0
  73. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  74. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  75. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  76. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  77. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  78. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +88 -0
  79. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  80. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +252 -0
  81. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  82. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +118 -0
  83. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  84. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  85. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  86. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  87. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  88. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  89. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  90. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  91. package/src/extensions/nocobase-ai/components/index.ts +59 -0
  92. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  93. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  94. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  95. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  96. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  97. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  98. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  99. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  100. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  101. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  102. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  103. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  104. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  105. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  106. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  107. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  108. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  109. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  110. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  111. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  112. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  113. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  114. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  115. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  116. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  117. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  118. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +49 -0
  119. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +210 -0
  120. package/src/extensions/nocobase-ai/demo/floating.tsx +134 -0
  121. package/src/extensions/nocobase-ai/demo/index.tsx +346 -0
  122. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  123. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  124. package/src/extensions/nocobase-ai/demo/page-context.tsx +884 -0
  125. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  126. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +41 -0
  127. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +452 -0
  128. package/src/extensions/nocobase-ai/demo/shortcut.tsx +1207 -0
  129. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +530 -0
  130. package/src/extensions/nocobase-ai/extension.tsx +158 -0
  131. package/src/extensions/nocobase-ai/global-ai-chat.tsx +213 -0
  132. package/src/extensions/nocobase-ai/index.ts +3 -0
  133. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +323 -0
  134. package/src/extensions/nocobase-ai/providers/avatars.ts +817 -0
  135. package/src/extensions/nocobase-ai/providers/chat-context.tsx +109 -0
  136. package/src/extensions/nocobase-ai/providers/chat-controller.ts +84 -0
  137. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +198 -0
  138. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +871 -0
  139. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +147 -0
  140. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +105 -0
  141. package/src/extensions/nocobase-ai/providers/chat-transport.ts +184 -0
  142. package/src/extensions/nocobase-ai/providers/form-registry.tsx +271 -0
  143. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  144. package/src/extensions/nocobase-ai/providers/index.ts +61 -0
  145. package/src/extensions/nocobase-ai/providers/model.ts +32 -0
  146. package/src/extensions/nocobase-ai/providers/page-context.tsx +136 -0
  147. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +60 -0
  148. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +82 -0
  149. package/src/extensions/nocobase-ai/providers/stream-parser.ts +61 -0
  150. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  151. package/src/extensions/nocobase-ai/providers/types.ts +183 -0
  152. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +463 -0
  153. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  154. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  155. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  156. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  157. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  158. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  159. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +178 -0
  160. package/src/extensions/nocobase-ai/services/index.ts +7 -0
  161. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  162. package/src/extensions/nocobase-ai/services/types.ts +82 -0
  163. package/src/index.tsx +2 -3
  164. package/src/lib/i18n.ts +161 -0
  165. package/src/lib/nocobase/client.ts +19 -2
  166. package/src/locales/en-US.ts +82 -0
  167. package/src/locales/index.ts +13 -0
  168. package/src/locales/zh-CN.ts +79 -0
  169. package/src/pages/users/create.tsx +98 -0
  170. package/src/pages/users/edit.tsx +92 -0
  171. package/src/pages/users/form-context.ts +57 -0
  172. package/src/pages/users/form-fields.tsx +211 -0
  173. package/src/pages/users/list.tsx +205 -0
  174. package/src/pages/users/show.tsx +187 -0
  175. package/src/pages/users/types.ts +20 -0
  176. package/src/providers/constants.ts +1 -0
  177. package/src/pages/blog-posts/create.tsx +0 -177
  178. package/src/pages/blog-posts/edit.tsx +0 -197
  179. package/src/pages/blog-posts/list.tsx +0 -131
  180. package/src/pages/blog-posts/show.tsx +0 -81
  181. package/src/pages/categories/create.tsx +0 -73
  182. package/src/pages/categories/edit.tsx +0 -73
  183. package/src/pages/categories/index.ts +0 -4
  184. package/src/pages/categories/list.tsx +0 -86
  185. package/src/pages/categories/show.tsx +0 -37
  186. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -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,8 @@ 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";
32
- import {
33
- filterMenuItemsByAcl,
34
- useAclSnapshot,
35
- } from "@/lib/nocobase/acl";
34
+ import { filterMenuItemsByAcl, useAclSnapshot } from "@/lib/nocobase/acl";
35
+ import { getResourceLabel } from "@/components/resources/resource-label";
36
36
 
37
37
  export function Sidebar() {
38
38
  const { open } = useShadcnSidebar();
@@ -102,6 +102,7 @@ function SidebarItem({ item, selectedKey }: MenuItemProps) {
102
102
  function SidebarItemGroup({ item, selectedKey }: MenuItemProps) {
103
103
  const { children } = item;
104
104
  const { open } = useShadcnSidebar();
105
+ const displayName = useMenuItemLabel(item);
105
106
 
106
107
  return (
107
108
  <div className={cn("mt-2 border-t", "border-sidebar-border/70", "pt-4")}>
@@ -125,7 +126,7 @@ function SidebarItemGroup({ item, selectedKey }: MenuItemProps) {
125
126
  }
126
127
  )}
127
128
  >
128
- {getDisplayName(item)}
129
+ {displayName}
129
130
  </span>
130
131
  {children && children.length > 0 && (
131
132
  <div className={cn("flex", "flex-col")}>
@@ -199,33 +200,44 @@ function SidebarItemDropdown({ item, selectedKey }: MenuItemProps) {
199
200
  render={<SidebarButton item={item} isSelected={isSelected} />}
200
201
  />
201
202
  <DropdownMenuContent side="right" align="start">
202
- {children?.map((child: TreeMenuItem) => {
203
- const { key: childKey } = child;
204
- const isSelected = childKey === selectedKey;
205
-
206
- return (
207
- <DropdownMenuItem
208
- key={childKey || child.name}
209
- render={<Link
210
- to={child.route || ""}
211
- className={cn("flex w-full items-center gap-2", {
212
- "bg-accent text-accent-foreground": isSelected,
213
- })}
214
- />}
215
- >
216
- <ItemIcon
217
- icon={child.meta?.icon ?? child.icon}
218
- isSelected={isSelected}
219
- />
220
- <span>{getDisplayName(child)}</span>
221
- </DropdownMenuItem>
222
- );
223
- })}
203
+ {children?.map((child: TreeMenuItem) => (
204
+ <SidebarDropdownItem
205
+ key={child.key || child.name}
206
+ item={child}
207
+ selectedKey={selectedKey}
208
+ Link={Link}
209
+ />
210
+ ))}
224
211
  </DropdownMenuContent>
225
212
  </DropdownMenu>
226
213
  );
227
214
  }
228
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
+
229
241
  function SidebarItemLink({ item, selectedKey }: MenuItemProps) {
230
242
  const isSelected = item.key === selectedKey;
231
243
 
@@ -250,7 +262,10 @@ function SidebarHeader() {
250
262
  >
251
263
  <Brand
252
264
  showText={open}
253
- logoClassName={cn("transition-transform duration-200", !open && "size-9")}
265
+ logoClassName={cn(
266
+ "transition-transform duration-200",
267
+ !open && "size-9"
268
+ )}
254
269
  />
255
270
  </ShadcnSidebarHeader>
256
271
  );
@@ -258,6 +273,7 @@ function SidebarHeader() {
258
273
 
259
274
  function SidebarFooter() {
260
275
  const { open } = useShadcnSidebar();
276
+ const translate = useTranslate();
261
277
 
262
278
  return (
263
279
  <ShadcnSidebarFooter className="border-t border-sidebar-border/70 p-0">
@@ -271,10 +287,13 @@ function SidebarFooter() {
271
287
  {open && (
272
288
  <div className="min-w-0 text-xs leading-4">
273
289
  <div className="font-semibold text-sidebar-foreground">
274
- AI builds freely.
290
+ {translate("shell.footer.freedom", "AI builds freely.")}
275
291
  </div>
276
292
  <div className="text-muted-foreground">
277
- NocoBase keeps it reliable.
293
+ {translate(
294
+ "shell.footer.reliability",
295
+ "NocoBase keeps it reliable."
296
+ )}
278
297
  </div>
279
298
  </div>
280
299
  )}
@@ -283,14 +302,22 @@ function SidebarFooter() {
283
302
  );
284
303
  }
285
304
 
286
- function getDisplayName(item: TreeMenuItem) {
287
- 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
+ );
288
316
  }
289
317
 
290
318
  function isTreeItemSelected(item: TreeMenuItem, selectedKey?: string) {
291
319
  return (
292
- item.key === selectedKey ||
293
- Boolean(selectedKey?.startsWith(`${item.key}/`))
320
+ item.key === selectedKey || Boolean(selectedKey?.startsWith(`${item.key}/`))
294
321
  );
295
322
  }
296
323
 
@@ -330,24 +357,22 @@ function SidebarButton({
330
357
  ...props
331
358
  }: SidebarButtonProps) {
332
359
  const Link = useLink();
360
+ const displayName = useMenuItemLabel(item);
333
361
 
334
362
  const buttonContent = (
335
363
  <>
336
364
  <ItemIcon icon={item.meta?.icon ?? item.icon} isSelected={isSelected} />
337
365
  <span
338
- className={cn(
339
- "tracking-[-0.00875rem] text-foreground",
340
- {
341
- "flex-1": rightIcon,
342
- "text-left": rightIcon,
343
- "line-clamp-1": !rightIcon,
344
- truncate: !rightIcon,
345
- "font-normal": !isSelected,
346
- "font-medium": isSelected,
347
- },
348
- )}
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
+ })}
349
374
  >
350
- {getDisplayName(item)}
375
+ {displayName}
351
376
  </span>
352
377
  {rightIcon}
353
378
  </>
@@ -357,7 +382,10 @@ function SidebarButton({
357
382
  <Button
358
383
  render={
359
384
  asLink && item.route ? (
360
- <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
+ />
361
389
  ) : undefined
362
390
  }
363
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
+ }
@@ -1,8 +1,7 @@
1
1
  "use client";
2
2
 
3
- import type { HttpError, BaseRecord } from "@refinedev/core";
3
+ import { useTranslate, type HttpError, type BaseRecord } from "@refinedev/core";
4
4
  import type { UseTableReturnType } from "@refinedev/react-table";
5
- import type { Column } from "@tanstack/react-table";
6
5
  import { flexRender } from "@tanstack/react-table";
7
6
  import { Loader2 } from "lucide-react";
8
7
  import { useEffect, useRef, useState } from "react";
@@ -16,6 +15,7 @@ import {
16
15
  TableRow,
17
16
  } from "@/components/ui/table";
18
17
  import { DataTablePagination } from "@/components/data-table/data-table-pagination";
18
+ import { getCommonStyles } from "@/components/data-table/data-table-styles";
19
19
  import { cn } from "@/lib/utils";
20
20
 
21
21
  type DataTableProps<TData extends BaseRecord> = {
@@ -25,6 +25,7 @@ type DataTableProps<TData extends BaseRecord> = {
25
25
  export function DataTable<TData extends BaseRecord>({
26
26
  table,
27
27
  }: DataTableProps<TData>) {
28
+ const translate = useTranslate();
28
29
  const {
29
30
  reactTable: { getHeaderGroups, getRowModel, getAllColumns },
30
31
  refineCore: {
@@ -198,6 +199,11 @@ export function DataTable<TData extends BaseRecord>({
198
199
  <DataTableNoData
199
200
  isOverflowing={isOverflowing}
200
201
  columnsLength={columns.length}
202
+ title={translate("table.empty.title", "No data to display")}
203
+ description={translate(
204
+ "table.empty.description",
205
+ "This table is empty for the time being."
206
+ )}
201
207
  />
202
208
  )}
203
209
  </TableBody>
@@ -220,9 +226,13 @@ export function DataTable<TData extends BaseRecord>({
220
226
  function DataTableNoData({
221
227
  isOverflowing,
222
228
  columnsLength,
229
+ title,
230
+ description,
223
231
  }: {
224
232
  isOverflowing: { horizontal: boolean; vertical: boolean };
225
233
  columnsLength: number;
234
+ title: string;
235
+ description: string;
226
236
  }) {
227
237
  return (
228
238
  <TableRow className="hover:bg-transparent">
@@ -252,10 +262,10 @@ function DataTableNoData({
252
262
  }}
253
263
  >
254
264
  <div className={cn("text-lg", "font-semibold", "text-foreground")}>
255
- No data to display
265
+ {title}
256
266
  </div>
257
267
  <div className={cn("text-sm", "text-muted-foreground")}>
258
- This table is empty for the time being.
268
+ {description}
259
269
  </div>
260
270
  </div>
261
271
  </TableCell>
@@ -263,59 +273,4 @@ function DataTableNoData({
263
273
  );
264
274
  }
265
275
 
266
- export function getCommonStyles<TData>({
267
- column,
268
- isOverflowing,
269
- }: {
270
- column: Column<TData>;
271
- isOverflowing: {
272
- horizontal: boolean;
273
- vertical: boolean;
274
- };
275
- }): React.CSSProperties {
276
- const isPinned = column.getIsPinned();
277
- const isLastLeftPinnedColumn =
278
- isPinned === "left" && column.getIsLastColumn("left");
279
- const isFirstRightPinnedColumn =
280
- isPinned === "right" && column.getIsFirstColumn("right");
281
-
282
- return {
283
- boxShadow:
284
- isOverflowing.horizontal && isLastLeftPinnedColumn
285
- ? "-4px 0 4px -4px var(--border) inset"
286
- : isOverflowing.horizontal && isFirstRightPinnedColumn
287
- ? "4px 0 4px -4px var(--border) inset"
288
- : undefined,
289
- left:
290
- isOverflowing.horizontal && isPinned === "left"
291
- ? `${column.getStart("left")}px`
292
- : undefined,
293
- right:
294
- isOverflowing.horizontal && isPinned === "right"
295
- ? `${column.getAfter("right")}px`
296
- : undefined,
297
- opacity: 1,
298
- position: isOverflowing.horizontal && isPinned ? "sticky" : "relative",
299
- background: isOverflowing.horizontal && isPinned ? "var(--background)" : "",
300
- borderTopRightRadius:
301
- isOverflowing.horizontal && isPinned === "right"
302
- ? "var(--radius)"
303
- : undefined,
304
- borderBottomRightRadius:
305
- isOverflowing.horizontal && isPinned === "right"
306
- ? "var(--radius)"
307
- : undefined,
308
- borderTopLeftRadius:
309
- isOverflowing.horizontal && isPinned === "left"
310
- ? "var(--radius)"
311
- : undefined,
312
- borderBottomLeftRadius:
313
- isOverflowing.horizontal && isPinned === "left"
314
- ? "var(--radius)"
315
- : undefined,
316
- width: column.getSize(),
317
- zIndex: isOverflowing.horizontal && isPinned ? 1 : 0,
318
- };
319
- }
320
-
321
276
  DataTable.displayName = "DataTable";
@@ -0,0 +1,101 @@
1
+ import {
2
+ type IResourceItem,
3
+ useTranslate,
4
+ useUserFriendlyName,
5
+ } from "@refinedev/core";
6
+
7
+ import {
8
+ parseTranslationExpression,
9
+ type TranslationOptions,
10
+ } from "@/lib/i18n";
11
+
12
+ export type ResourceLabelForm = "plural" | "singular";
13
+
14
+ type ResourceLabelMeta = {
15
+ i18nKey?: string;
16
+ i18nOptions?: TranslationOptions;
17
+ i18nSingularKey?: string;
18
+ label?: unknown;
19
+ singularLabel?: unknown;
20
+ };
21
+
22
+ type Translate = ReturnType<typeof useTranslate>;
23
+ type UserFriendlyName = ReturnType<typeof useUserFriendlyName>;
24
+
25
+ function translateWithFallback(
26
+ translate: Translate,
27
+ key: string,
28
+ fallback: string,
29
+ options?: TranslationOptions
30
+ ) {
31
+ const translated = options
32
+ ? translate(key, options, fallback)
33
+ : translate(key, fallback);
34
+
35
+ return !translated || translated === key ? fallback : translated;
36
+ }
37
+
38
+ function resolveExplicitLabel(value: unknown, translate: Translate) {
39
+ if (typeof value !== "string") {
40
+ return value == null ? undefined : String(value);
41
+ }
42
+
43
+ const expression = parseTranslationExpression(value);
44
+ if (!expression) return value;
45
+
46
+ const fallback = expression.options?.defaultValue ?? expression.key;
47
+ return translateWithFallback(
48
+ translate,
49
+ expression.key,
50
+ fallback,
51
+ expression.options
52
+ );
53
+ }
54
+
55
+ export function getResourceLabel(
56
+ resource: IResourceItem | undefined,
57
+ form: ResourceLabelForm,
58
+ translate: Translate,
59
+ getUserFriendlyName: UserFriendlyName,
60
+ fallbackIdentifier?: string
61
+ ) {
62
+ const meta = resource?.meta as ResourceLabelMeta | undefined;
63
+ const topLevelLabel = (
64
+ resource as (IResourceItem & { label?: unknown }) | undefined
65
+ )?.label;
66
+ const explicitLabel =
67
+ form === "singular"
68
+ ? meta?.singularLabel ?? meta?.label ?? topLevelLabel
69
+ : meta?.label ?? topLevelLabel;
70
+ const fallback =
71
+ resolveExplicitLabel(explicitLabel, translate) ??
72
+ getUserFriendlyName(
73
+ fallbackIdentifier ?? resource?.identifier ?? resource?.name ?? "",
74
+ form
75
+ );
76
+ const i18nKey =
77
+ form === "singular"
78
+ ? meta?.i18nSingularKey ?? meta?.i18nKey
79
+ : meta?.i18nKey;
80
+
81
+ return i18nKey
82
+ ? translateWithFallback(translate, i18nKey, fallback, meta?.i18nOptions)
83
+ : fallback;
84
+ }
85
+
86
+ export function useResourceLabel(
87
+ resource: IResourceItem | undefined,
88
+ form: ResourceLabelForm,
89
+ fallbackIdentifier?: string
90
+ ) {
91
+ const translate = useTranslate();
92
+ const getUserFriendlyName = useUserFriendlyName();
93
+
94
+ return getResourceLabel(
95
+ resource,
96
+ form,
97
+ translate,
98
+ getUserFriendlyName,
99
+ fallbackIdentifier
100
+ );
101
+ }
@@ -1,15 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  import { cn } from "@/lib/utils";
4
- import {
5
- useBack,
6
- useResourceParams,
7
- useUserFriendlyName,
8
- } from "@refinedev/core";
4
+ import { useBack, useResourceParams, useTranslate } from "@refinedev/core";
9
5
  import type { PropsWithChildren } from "react";
10
6
  import { Breadcrumb } from "@/components/app-shell/breadcrumb";
11
7
  import { Button } from "@/components/ui/button";
12
8
  import { ArrowLeftIcon } from "lucide-react";
9
+ import { useResourceLabel } from "@/components/resources/resource-label";
13
10
 
14
11
  type CreateViewProps = PropsWithChildren<{
15
12
  className?: string;
@@ -38,18 +35,25 @@ export const CreateViewHeader = ({
38
35
  headerClassName,
39
36
  }: CreateHeaderProps) => {
40
37
  const back = useBack();
41
-
42
- const getUserFriendlyName = useUserFriendlyName();
38
+ const translate = useTranslate();
43
39
 
44
40
  const { resource, identifier } = useResourceParams({
45
41
  resource: resourceFromProps,
46
42
  });
47
43
 
48
- const resourceTitle = getUserFriendlyName(
49
- resource?.meta?.label ?? identifier ?? resource?.name,
50
- "singular"
44
+ const resourceTitle = useResourceLabel(resource, "singular", identifier);
45
+ const title =
46
+ titleFromProps ??
47
+ translate(
48
+ "views.create.title",
49
+ { resource: resourceTitle },
50
+ `Create ${resourceTitle}`
51
+ );
52
+ const description = translate(
53
+ "views.create.description",
54
+ { resource: resourceTitle.toLocaleLowerCase() },
55
+ `Add a new ${resourceTitle.toLocaleLowerCase()} to your NocoBase workspace.`
51
56
  );
52
- const title = titleFromProps ?? `Create ${resourceTitle}`;
53
57
 
54
58
  return (
55
59
  <div className={cn("flex flex-col", "gap-3", wrapperClassName)}>
@@ -62,6 +66,7 @@ export const CreateViewHeader = ({
62
66
  size="icon"
63
67
  className="mt-0.5 rounded-lg"
64
68
  onClick={back}
69
+ aria-label={translate("buttons.cancel", "Cancel")}
65
70
  >
66
71
  <ArrowLeftIcon className="h-4 w-4" />
67
72
  </Button>
@@ -69,9 +74,7 @@ export const CreateViewHeader = ({
69
74
  <h2 className="text-3xl font-semibold tracking-[-0.035em]">
70
75
  {title}
71
76
  </h2>
72
- <p className="mt-2 text-sm text-muted-foreground">
73
- Add a new {resourceTitle.toLowerCase()} to your NocoBase workspace.
74
- </p>
77
+ <p className="mt-2 text-sm text-muted-foreground">{description}</p>
75
78
  </div>
76
79
  </div>
77
80
  </div>