@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.
- package/README.MD +22 -3
- package/package.json +4 -2
- package/registry/nocobase-acl/components/role-options.ts +7 -0
- package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
- package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
- package/registry/nocobase-acl/demo/components.tsx +32 -11
- package/registry/nocobase-acl/demo/index.tsx +45 -14
- package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
- package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
- package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
- package/registry/nocobase-acl/extension.tsx +1 -1
- package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
- package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
- package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
- package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
- package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
- package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
- package/registry/nocobase-ai/extension.tsx +2 -7
- package/registry/nocobase-i18n/README.md +53 -0
- package/registry/nocobase-i18n/components/index.ts +1 -0
- package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
- package/registry/nocobase-i18n/demo/index.tsx +209 -0
- package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
- package/registry/nocobase-i18n/extension.tsx +46 -0
- package/registry/nocobase-i18n/i18n-provider.ts +9 -0
- package/registry/nocobase-i18n/index.ts +6 -0
- package/registry/nocobase-i18n/locale-store.ts +134 -0
- package/registry/nocobase-i18n/locales/en-US.ts +89 -0
- package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
- package/registry/nocobase-i18n/provider.tsx +32 -0
- package/registry/nocobase-i18n/runtime.ts +118 -0
- package/registry/nocobase-i18n/server-resources.ts +184 -0
- package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
- package/registry.config.json +34 -14
- package/scripts/registry.mjs +5 -0
- package/src/App.tsx +27 -86
- package/src/app/extension.ts +3 -1
- package/src/app/extensions.tsx +21 -0
- package/src/components/access-control/acl-gate.tsx +4 -10
- package/src/components/app-shell/breadcrumb.tsx +39 -3
- package/src/components/app-shell/document-title-handler.tsx +11 -20
- package/src/components/app-shell/header.tsx +18 -3
- package/src/components/app-shell/loading-overlay.tsx +2 -4
- package/src/components/app-shell/loading-state.tsx +15 -0
- package/src/components/app-shell/sidebar.tsx +75 -47
- package/src/components/data-table/data-table-pagination.tsx +27 -7
- package/src/components/data-table/data-table-styles.ts +57 -0
- package/src/components/data-table/data-table.tsx +14 -59
- package/src/components/resources/resource-label.ts +101 -0
- package/src/components/resources/views/create-view.tsx +17 -14
- package/src/components/resources/views/edit-view.tsx +17 -15
- package/src/components/resources/views/list-view.tsx +20 -10
- package/src/components/resources/views/show-view.tsx +20 -18
- package/src/extensions/nocobase-acl/README.md +19 -0
- package/src/extensions/nocobase-acl/components/acl-boundary.tsx +106 -0
- package/src/extensions/nocobase-acl/components/index.ts +3 -0
- package/src/extensions/nocobase-acl/components/role-options.ts +36 -0
- package/src/extensions/nocobase-acl/components/role-switcher.tsx +135 -0
- package/src/extensions/nocobase-acl/demo/boundary-api.tsx +186 -0
- package/src/extensions/nocobase-acl/demo/components.tsx +231 -0
- package/src/extensions/nocobase-acl/demo/index.tsx +505 -0
- package/src/extensions/nocobase-acl/demo/lazy-route.tsx +10 -0
- package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +134 -0
- package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
- package/src/extensions/nocobase-acl/demo/scenario-section.tsx +37 -0
- package/src/extensions/nocobase-acl/extension.tsx +71 -0
- package/src/extensions/nocobase-acl/index.ts +1 -0
- package/src/extensions/nocobase-ai/README.md +70 -0
- package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +20 -0
- package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
- package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +32 -0
- package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
- package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
- package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +90 -0
- package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +429 -0
- package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
- package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +88 -0
- package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
- package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +252 -0
- package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +173 -0
- package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +118 -0
- package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +343 -0
- package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +165 -0
- package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +30 -0
- package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
- package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
- package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
- package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
- package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
- package/src/extensions/nocobase-ai/components/index.ts +59 -0
- package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
- package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
- package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
- package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
- package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
- package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
- package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
- package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
- package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
- package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
- package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
- package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
- package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
- package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +49 -0
- package/src/extensions/nocobase-ai/demo/container-showcase.tsx +210 -0
- package/src/extensions/nocobase-ai/demo/floating.tsx +134 -0
- package/src/extensions/nocobase-ai/demo/index.tsx +346 -0
- package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +151 -0
- package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
- package/src/extensions/nocobase-ai/demo/page-context.tsx +884 -0
- package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +197 -0
- package/src/extensions/nocobase-ai/demo/prompt-card.tsx +41 -0
- package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +452 -0
- package/src/extensions/nocobase-ai/demo/shortcut.tsx +1207 -0
- package/src/extensions/nocobase-ai/demo/tool-cards.tsx +530 -0
- package/src/extensions/nocobase-ai/extension.tsx +158 -0
- package/src/extensions/nocobase-ai/global-ai-chat.tsx +213 -0
- package/src/extensions/nocobase-ai/index.ts +3 -0
- package/src/extensions/nocobase-ai/providers/ai-provider.tsx +323 -0
- package/src/extensions/nocobase-ai/providers/avatars.ts +817 -0
- package/src/extensions/nocobase-ai/providers/chat-context.tsx +109 -0
- package/src/extensions/nocobase-ai/providers/chat-controller.ts +84 -0
- package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +198 -0
- package/src/extensions/nocobase-ai/providers/chat-provider.tsx +871 -0
- package/src/extensions/nocobase-ai/providers/chat-reducer.ts +147 -0
- package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +105 -0
- package/src/extensions/nocobase-ai/providers/chat-transport.ts +184 -0
- package/src/extensions/nocobase-ai/providers/form-registry.tsx +271 -0
- package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
- package/src/extensions/nocobase-ai/providers/index.ts +61 -0
- package/src/extensions/nocobase-ai/providers/model.ts +32 -0
- package/src/extensions/nocobase-ai/providers/page-context.tsx +136 -0
- package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +60 -0
- package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +82 -0
- package/src/extensions/nocobase-ai/providers/stream-parser.ts +61 -0
- package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +316 -0
- package/src/extensions/nocobase-ai/providers/types.ts +183 -0
- package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +463 -0
- package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
- package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +206 -0
- package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
- package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +210 -0
- package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +93 -0
- package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
- package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +178 -0
- package/src/extensions/nocobase-ai/services/index.ts +7 -0
- package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +454 -0
- package/src/extensions/nocobase-ai/services/types.ts +82 -0
- package/src/index.tsx +2 -3
- package/src/lib/i18n.ts +161 -0
- package/src/lib/nocobase/client.ts +19 -2
- package/src/locales/en-US.ts +82 -0
- package/src/locales/index.ts +13 -0
- package/src/locales/zh-CN.ts +79 -0
- package/src/pages/users/create.tsx +98 -0
- package/src/pages/users/edit.tsx +92 -0
- package/src/pages/users/form-context.ts +57 -0
- package/src/pages/users/form-fields.tsx +211 -0
- package/src/pages/users/list.tsx +205 -0
- package/src/pages/users/show.tsx +187 -0
- package/src/pages/users/types.ts +20 -0
- package/src/providers/constants.ts +1 -0
- package/src/pages/blog-posts/create.tsx +0 -177
- package/src/pages/blog-posts/edit.tsx +0 -197
- package/src/pages/blog-posts/list.tsx +0 -131
- package/src/pages/blog-posts/show.tsx +0 -81
- package/src/pages/categories/create.tsx +0 -73
- package/src/pages/categories/edit.tsx +0 -73
- package/src/pages/categories/index.ts +0 -4
- package/src/pages/categories/list.tsx +0 -86
- package/src/pages/categories/show.tsx +0 -37
- /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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
287
|
-
|
|
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
|
-
"
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
{
|
|
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
|
|
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"
|
|
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")}>
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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
|
|
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
|
-
|
|
265
|
+
{title}
|
|
256
266
|
</div>
|
|
257
267
|
<div className={cn("text-sm", "text-muted-foreground")}>
|
|
258
|
-
|
|
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 =
|
|
49
|
-
|
|
50
|
-
|
|
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>
|