@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.
- package/README.MD +90 -1
- package/components.json +3 -1
- package/package.json +21 -5
- package/registry/nocobase-acl/README.md +19 -0
- package/registry/nocobase-acl/components/acl-boundary.tsx +106 -0
- package/registry/nocobase-acl/components/index.ts +3 -0
- package/registry/nocobase-acl/components/role-options.ts +36 -0
- package/registry/nocobase-acl/components/role-switcher.tsx +135 -0
- package/registry/nocobase-acl/demo/boundary-api.tsx +186 -0
- package/registry/nocobase-acl/demo/components.tsx +231 -0
- package/registry/nocobase-acl/demo/index.tsx +505 -0
- package/registry/nocobase-acl/demo/lazy-route.tsx +10 -0
- package/registry/nocobase-acl/demo/prompt-generator.tsx +134 -0
- package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
- package/registry/nocobase-acl/demo/scenario-section.tsx +37 -0
- package/registry/nocobase-acl/extension.tsx +71 -0
- package/registry/nocobase-acl/index.ts +1 -0
- package/registry/nocobase-acl/tests/acl-regression.mjs +314 -0
- package/registry/nocobase-ai/README.md +70 -0
- package/registry/nocobase-ai/adapters/react-hook-form.ts +20 -0
- package/registry/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
- package/registry/nocobase-ai/components/ai-root-provider.tsx +32 -0
- package/registry/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
- package/registry/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
- package/registry/nocobase-ai/components/chat/chat-compact.tsx +90 -0
- package/registry/nocobase-ai/components/chat/chat-composer.tsx +429 -0
- package/registry/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
- package/registry/nocobase-ai/components/chat/chat-header.tsx +88 -0
- package/registry/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
- package/registry/nocobase-ai/components/chat/chat-message.tsx +252 -0
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +173 -0
- package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
- package/registry/nocobase-ai/components/chat/conversation-list.tsx +343 -0
- package/registry/nocobase-ai/components/chat/markdown-message.tsx +165 -0
- package/registry/nocobase-ai/components/chat/model-select-options.tsx +30 -0
- package/registry/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
- package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
- package/registry/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
- package/registry/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
- package/registry/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
- package/registry/nocobase-ai/components/index.ts +59 -0
- package/registry/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
- package/registry/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
- package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
- package/registry/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
- package/registry/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
- package/registry/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
- package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
- package/registry/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
- package/registry/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
- package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
- package/registry/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
- package/registry/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
- package/registry/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
- package/registry/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
- package/registry/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
- package/registry/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
- package/registry/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
- package/registry/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
- package/registry/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
- package/registry/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
- package/registry/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
- package/registry/nocobase-ai/demo/configuration-gate.tsx +49 -0
- package/registry/nocobase-ai/demo/container-showcase.tsx +210 -0
- package/registry/nocobase-ai/demo/floating.tsx +134 -0
- package/registry/nocobase-ai/demo/index.tsx +346 -0
- package/registry/nocobase-ai/demo/interaction-showcase.tsx +151 -0
- package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
- package/registry/nocobase-ai/demo/page-context.tsx +884 -0
- package/registry/nocobase-ai/demo/page-element-showcase.tsx +197 -0
- package/registry/nocobase-ai/demo/prompt-card.tsx +41 -0
- package/registry/nocobase-ai/demo/prompt-generator.tsx +452 -0
- package/registry/nocobase-ai/demo/shortcut.tsx +1207 -0
- package/registry/nocobase-ai/demo/tool-cards.tsx +530 -0
- package/registry/nocobase-ai/extension.tsx +158 -0
- package/registry/nocobase-ai/global-ai-chat.tsx +213 -0
- package/registry/nocobase-ai/index.ts +3 -0
- package/registry/nocobase-ai/providers/ai-provider.tsx +323 -0
- package/registry/nocobase-ai/providers/avatars.ts +817 -0
- package/registry/nocobase-ai/providers/chat-context.tsx +109 -0
- package/registry/nocobase-ai/providers/chat-controller.ts +84 -0
- package/registry/nocobase-ai/providers/chat-message-utils.ts +198 -0
- package/registry/nocobase-ai/providers/chat-provider.tsx +871 -0
- package/registry/nocobase-ai/providers/chat-reducer.ts +147 -0
- package/registry/nocobase-ai/providers/chat-task-utils.ts +105 -0
- package/registry/nocobase-ai/providers/chat-transport.ts +184 -0
- package/registry/nocobase-ai/providers/form-registry.tsx +271 -0
- package/registry/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
- package/registry/nocobase-ai/providers/index.ts +61 -0
- package/registry/nocobase-ai/providers/model.ts +32 -0
- package/registry/nocobase-ai/providers/page-context.tsx +136 -0
- package/registry/nocobase-ai/providers/stream-coalescer.ts +60 -0
- package/registry/nocobase-ai/providers/stream-event-utils.ts +82 -0
- package/registry/nocobase-ai/providers/stream-parser.ts +61 -0
- package/registry/nocobase-ai/providers/sub-agent-stream.ts +316 -0
- package/registry/nocobase-ai/providers/types.ts +183 -0
- package/registry/nocobase-ai/providers/ui-message-stream.ts +463 -0
- package/registry/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
- package/registry/nocobase-ai/providers/use-chat-attachments.ts +206 -0
- package/registry/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
- package/registry/nocobase-ai/providers/use-chat-runtime.ts +210 -0
- package/registry/nocobase-ai/providers/use-chat-work-context.ts +93 -0
- package/registry/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
- package/registry/nocobase-ai/providers/use-conversation-history.ts +178 -0
- package/registry/nocobase-ai/services/index.ts +7 -0
- package/registry/nocobase-ai/services/nocobase-ai-service.ts +454 -0
- package/registry/nocobase-ai/services/types.ts +82 -0
- package/registry/nocobase-ai/tests/business-report-regression.mjs +70 -0
- package/registry/nocobase-ai/tests/form-filler-regression.mjs +129 -0
- package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +128 -0
- package/registry/nocobase-ai/tests/page-context-regression.mjs +48 -0
- package/registry/nocobase-ai/tests/stream-regression.mjs +207 -0
- 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 +163 -0
- package/scripts/registry.mjs +164 -0
- package/src/App.css +95 -0
- package/src/App.tsx +87 -69
- package/src/app/extension.ts +11 -0
- package/src/app/extensions.tsx +49 -0
- package/src/components/access-control/access-denied.tsx +30 -0
- package/src/components/access-control/acl-bootstrap.tsx +9 -0
- package/src/components/access-control/acl-gate.tsx +36 -0
- package/src/components/access-control/navigate-to-accessible-resource.tsx +17 -0
- package/src/components/access-control/resource-access-guard.tsx +21 -0
- package/src/components/access-control/use-acl-runtime.ts +28 -0
- 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 +22 -4
- 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 +103 -45
- 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/index.tsx +2 -3
- package/src/lib/i18n.ts +161 -0
- package/src/lib/nocobase/acl/action.ts +147 -0
- package/src/lib/nocobase/acl/data-source.ts +24 -0
- package/src/lib/nocobase/acl/index.ts +6 -0
- package/src/lib/nocobase/acl/menu.ts +41 -0
- package/src/lib/nocobase/acl/record-permissions.ts +111 -0
- package/src/lib/nocobase/acl/store.ts +182 -0
- package/src/lib/nocobase/acl/types.ts +96 -0
- package/src/lib/nocobase/client.ts +279 -0
- package/src/lib/nocobase/error.ts +32 -0
- 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/access-control.ts +34 -0
- package/src/providers/auth.ts +74 -60
- package/src/providers/constants.ts +3 -37
- package/src/providers/data.ts +242 -85
- package/vite.config.ts +17 -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
|
@@ -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-${
|
|
71
|
+
key: `breadcrumb-item-${displayLabel}`,
|
|
47
72
|
href: href ?? "",
|
|
48
|
-
Component: href ?
|
|
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
|
-
}, [
|
|
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
|
|
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 {
|
|
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
|
-
<
|
|
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>
|
|
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
|
-
<
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
165
|
+
<Collapsible
|
|
166
|
+
key={`collapsible-${name}`}
|
|
167
|
+
defaultOpen={isSelected}
|
|
168
|
+
className={cn("w-full", "group")}
|
|
169
|
+
>
|
|
155
170
|
<CollapsibleTrigger
|
|
156
|
-
render={
|
|
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
|
|
199
|
+
<DropdownMenuTrigger
|
|
200
|
+
render={<SidebarButton item={item} isSelected={isSelected} />}
|
|
201
|
+
/>
|
|
178
202
|
<DropdownMenuContent side="right" align="start">
|
|
179
|
-
{children?.map((child: TreeMenuItem) =>
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
264
|
-
|
|
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
|
-
"
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
{
|
|
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
|
|
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"
|
|
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
|
+
}
|