@nocobase/portal-template-default 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +10 -5
- package/package.json +17 -17
- package/registry/nocobase-ai/components/chat/chat-message.tsx +8 -6
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +8 -3
- package/registry/nocobase-ai/components/chat/markdown-message.tsx +26 -0
- package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +3 -3
- package/registry/nocobase-ai/demo/configuration-gate.tsx +83 -35
- package/registry/nocobase-i18n/README.md +23 -32
- package/registry/nocobase-i18n/components/language-switcher.tsx +1 -1
- package/registry/nocobase-i18n/demo/index.tsx +2 -2
- package/registry/nocobase-i18n/demo/prompt-generator.tsx +3 -2
- package/registry/nocobase-i18n/extension.tsx +3 -4
- package/registry/nocobase-i18n/index.ts +4 -3
- package/registry/nocobase-i18n/locales/en-US.ts +2 -52
- package/registry/nocobase-i18n/locales/index.ts +8 -0
- package/registry/nocobase-i18n/locales/zh-CN.ts +2 -52
- package/registry/nocobase-i18n/provider.tsx +18 -6
- package/registry/nocobase-i18n/server-resources.ts +13 -31
- package/registry/nocobase-i18n/tests/i18n-regression.mjs +14 -5
- package/registry/nocobase-route-surfaces/README.md +21 -0
- package/registry/nocobase-route-surfaces/demo/index.tsx +266 -0
- package/{src/extensions/nocobase-acl → registry/nocobase-route-surfaces}/demo/lazy-route.tsx +2 -1
- package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +90 -0
- package/registry/nocobase-route-surfaces/demo/scenarios.ts +95 -0
- package/registry/nocobase-route-surfaces/extension.tsx +119 -0
- package/registry/nocobase-route-surfaces/index.ts +7 -0
- package/registry/nocobase-route-surfaces/route-dialog.tsx +145 -0
- package/registry/nocobase-route-surfaces/route-drawer.tsx +186 -0
- package/registry/nocobase-route-surfaces/route-page.tsx +29 -0
- package/registry/nocobase-route-surfaces/route-surface-context.ts +13 -0
- package/registry/nocobase-route-surfaces/use-refine-unsaved-changes.tsx +106 -0
- package/registry/nocobase-route-surfaces/use-route-surface-close.ts +15 -0
- package/registry/nocobase-route-surfaces/use-route-surface-state.ts +65 -0
- package/registry.config.json +30 -3
- package/scripts/registry.mjs +0 -2
- package/src/App.tsx +65 -35
- package/src/app/extension.ts +1 -2
- package/src/app/extensions.tsx +0 -14
- package/src/components/app-shell/breadcrumb.tsx +67 -2
- package/src/components/app-shell/sidebar.tsx +2 -2
- package/src/components/resources/views/list-view.tsx +3 -2
- package/src/components/ui/sheet.tsx +22 -21
- package/src/lib/nocobase/client.ts +6 -1
- package/src/locales/en-US.ts +81 -0
- package/src/locales/zh-CN.ts +77 -0
- package/src/pages/users/create.tsx +76 -53
- package/src/pages/users/edit.tsx +87 -56
- package/src/pages/users/index.ts +3 -0
- package/src/pages/users/layout.tsx +15 -0
- package/src/pages/users/list.tsx +19 -21
- package/src/pages/users/role-badges.tsx +43 -0
- package/src/pages/users/role-detail.tsx +272 -0
- package/src/pages/users/role-utils.ts +6 -0
- package/src/pages/users/routes.ts +24 -0
- package/src/pages/users/show.tsx +195 -104
- package/src/pages/users/types.ts +13 -0
- package/src/providers/i18n/index.ts +3 -0
- package/{registry/nocobase-i18n → src/providers/i18n}/runtime.ts +63 -16
- package/src/providers/system-settings/context.ts +26 -0
- package/src/providers/system-settings/index.ts +2 -0
- package/src/providers/system-settings/provider.tsx +94 -0
- package/src/extensions/nocobase-acl/README.md +0 -19
- package/src/extensions/nocobase-acl/components/acl-boundary.tsx +0 -106
- package/src/extensions/nocobase-acl/components/index.ts +0 -3
- package/src/extensions/nocobase-acl/components/role-options.ts +0 -36
- package/src/extensions/nocobase-acl/components/role-switcher.tsx +0 -135
- package/src/extensions/nocobase-acl/demo/boundary-api.tsx +0 -186
- package/src/extensions/nocobase-acl/demo/components.tsx +0 -231
- package/src/extensions/nocobase-acl/demo/index.tsx +0 -505
- package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +0 -134
- package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +0 -198
- package/src/extensions/nocobase-acl/demo/scenario-section.tsx +0 -37
- package/src/extensions/nocobase-acl/extension.tsx +0 -71
- package/src/extensions/nocobase-acl/index.ts +0 -1
- package/src/extensions/nocobase-ai/README.md +0 -70
- package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +0 -20
- package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +0 -50
- package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +0 -32
- package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +0 -31
- package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +0 -92
- package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +0 -90
- package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +0 -429
- package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +0 -51
- package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +0 -88
- package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +0 -77
- package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +0 -252
- package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +0 -173
- package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +0 -118
- package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +0 -343
- package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +0 -165
- package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +0 -30
- package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +0 -29
- package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +0 -142
- package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +0 -316
- package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +0 -99
- package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +0 -114
- package/src/extensions/nocobase-ai/components/index.ts +0 -59
- package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +0 -44
- package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +0 -469
- package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +0 -31
- package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +0 -18
- package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +0 -18
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +0 -54
- package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +0 -74
- package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +0 -54
- package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +0 -31
- package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +0 -364
- package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +0 -122
- package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +0 -282
- package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +0 -99
- package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +0 -59
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +0 -18
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +0 -20
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +0 -34
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +0 -24
- package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +0 -138
- package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +0 -67
- package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +0 -59
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +0 -55
- package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +0 -18
- package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +0 -108
- package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +0 -73
- package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +0 -149
- package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +0 -49
- package/src/extensions/nocobase-ai/demo/container-showcase.tsx +0 -210
- package/src/extensions/nocobase-ai/demo/floating.tsx +0 -134
- package/src/extensions/nocobase-ai/demo/index.tsx +0 -346
- package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +0 -151
- package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +0 -355
- package/src/extensions/nocobase-ai/demo/page-context.tsx +0 -884
- package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +0 -197
- package/src/extensions/nocobase-ai/demo/prompt-card.tsx +0 -41
- package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +0 -452
- package/src/extensions/nocobase-ai/demo/shortcut.tsx +0 -1207
- package/src/extensions/nocobase-ai/demo/tool-cards.tsx +0 -530
- package/src/extensions/nocobase-ai/extension.tsx +0 -158
- package/src/extensions/nocobase-ai/global-ai-chat.tsx +0 -213
- package/src/extensions/nocobase-ai/index.ts +0 -3
- package/src/extensions/nocobase-ai/providers/ai-provider.tsx +0 -323
- package/src/extensions/nocobase-ai/providers/avatars.ts +0 -817
- package/src/extensions/nocobase-ai/providers/chat-context.tsx +0 -109
- package/src/extensions/nocobase-ai/providers/chat-controller.ts +0 -84
- package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +0 -198
- package/src/extensions/nocobase-ai/providers/chat-provider.tsx +0 -871
- package/src/extensions/nocobase-ai/providers/chat-reducer.ts +0 -147
- package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +0 -105
- package/src/extensions/nocobase-ai/providers/chat-transport.ts +0 -184
- package/src/extensions/nocobase-ai/providers/form-registry.tsx +0 -271
- package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +0 -231
- package/src/extensions/nocobase-ai/providers/index.ts +0 -61
- package/src/extensions/nocobase-ai/providers/model.ts +0 -32
- package/src/extensions/nocobase-ai/providers/page-context.tsx +0 -136
- package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +0 -60
- package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +0 -82
- package/src/extensions/nocobase-ai/providers/stream-parser.ts +0 -61
- package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +0 -316
- package/src/extensions/nocobase-ai/providers/types.ts +0 -183
- package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +0 -463
- package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +0 -94
- package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +0 -206
- package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +0 -554
- package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +0 -210
- package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +0 -93
- package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +0 -108
- package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +0 -178
- package/src/extensions/nocobase-ai/services/index.ts +0 -7
- package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +0 -454
- package/src/extensions/nocobase-ai/services/types.ts +0 -82
- /package/{registry/nocobase-i18n → src/providers/i18n}/i18n-provider.ts +0 -0
- /package/{registry/nocobase-i18n → src/providers/i18n}/locale-store.ts +0 -0
|
@@ -1,53 +1,4 @@
|
|
|
1
1
|
const zhCN = {
|
|
2
|
-
starter: {
|
|
3
|
-
"shell.workspace": "AI 应用工作区",
|
|
4
|
-
"shell.footer.freedom": "AI 自由构建。",
|
|
5
|
-
"shell.footer.reliability": "NocoBase 保障可靠运行。",
|
|
6
|
-
"auth.signOut": "退出登录",
|
|
7
|
-
"auth.signingOut": "正在退出...",
|
|
8
|
-
"buttons.apply": "应用",
|
|
9
|
-
"buttons.clear": "清除",
|
|
10
|
-
"buttons.undo": "撤销",
|
|
11
|
-
"pages.error.title": "页面不存在",
|
|
12
|
-
"pages.error.description": "你访问的页面不存在。",
|
|
13
|
-
"pages.error.backHome": "返回首页",
|
|
14
|
-
"table.filter.text.placeholder": "输入筛选内容...",
|
|
15
|
-
"table.filter.numeric.placeholder": "输入筛选数值...",
|
|
16
|
-
"table.filter.combobox.placeholder": "请选择...",
|
|
17
|
-
"table.filter.combobox.search": "搜索...",
|
|
18
|
-
"table.filter.combobox.noResults": "没有找到结果。",
|
|
19
|
-
"table.filter.operator.placeholder": "搜索操作符...",
|
|
20
|
-
"table.filter.operator.noResults": "没有找到操作符。",
|
|
21
|
-
"table.filter.operator.eq": "等于",
|
|
22
|
-
"table.filter.operator.ne": "不等于",
|
|
23
|
-
"table.filter.operator.lt": "小于",
|
|
24
|
-
"table.filter.operator.gt": "大于",
|
|
25
|
-
"table.filter.operator.lte": "小于或等于",
|
|
26
|
-
"table.filter.operator.gte": "大于或等于",
|
|
27
|
-
"table.filter.operator.in": "包含于数组",
|
|
28
|
-
"table.filter.operator.nin": "不包含于数组",
|
|
29
|
-
"table.filter.operator.ina": "包含于数组(区分大小写)",
|
|
30
|
-
"table.filter.operator.nina": "不包含于数组(区分大小写)",
|
|
31
|
-
"table.filter.operator.contains": "包含",
|
|
32
|
-
"table.filter.operator.ncontains": "不包含",
|
|
33
|
-
"table.filter.operator.containss": "包含(区分大小写)",
|
|
34
|
-
"table.filter.operator.ncontainss": "不包含(区分大小写)",
|
|
35
|
-
"table.filter.operator.between": "介于",
|
|
36
|
-
"table.filter.operator.nbetween": "不介于",
|
|
37
|
-
"table.filter.operator.null": "为空",
|
|
38
|
-
"table.filter.operator.nnull": "不为空",
|
|
39
|
-
"table.filter.operator.startswith": "开头是",
|
|
40
|
-
"table.filter.operator.nstartswith": "开头不是",
|
|
41
|
-
"table.filter.operator.startswiths": "开头是(区分大小写)",
|
|
42
|
-
"table.filter.operator.nstartswiths": "开头不是(区分大小写)",
|
|
43
|
-
"table.filter.operator.endswith": "结尾是",
|
|
44
|
-
"table.filter.operator.nendswith": "结尾不是",
|
|
45
|
-
"table.filter.operator.endswiths": "结尾是(区分大小写)",
|
|
46
|
-
"table.filter.operator.nendswiths": "结尾不是(区分大小写)",
|
|
47
|
-
"table.filter.operator.eqs": "等于(区分大小写)",
|
|
48
|
-
"table.filter.operator.nes": "不等于(区分大小写)",
|
|
49
|
-
},
|
|
50
|
-
"nocobase-i18n": {
|
|
51
2
|
"language.label": "语言",
|
|
52
3
|
"language.current": "当前语言",
|
|
53
4
|
"language.switchError": "无法切换语言。",
|
|
@@ -55,7 +6,7 @@ const zhCN = {
|
|
|
55
6
|
"navigation.demo": "国际化",
|
|
56
7
|
"demo.title": "国际化",
|
|
57
8
|
"demo.description":
|
|
58
|
-
"
|
|
9
|
+
"基于 Starter 国际化运行时的可选 NocoBase 服务端翻译与语言切换能力。",
|
|
59
10
|
"demo.switcher.title": "语言切换器",
|
|
60
11
|
"demo.switcher.description": "同一个可复用组件也会集成到登录用户菜单中。",
|
|
61
12
|
"demo.switcher.unavailable.title": "当前仅启用一种语言",
|
|
@@ -63,7 +14,7 @@ const zhCN = {
|
|
|
63
14
|
"请在 NocoBase 系统设置中启用其他语言,语言切换器才会显示。",
|
|
64
15
|
"demo.frontend.title": "应用 UI 文案翻译",
|
|
65
16
|
"demo.frontend.description":
|
|
66
|
-
"Starter
|
|
17
|
+
"Starter 提供运行时,应用和 Registry 组件分别维护自己的本地语言资源。",
|
|
67
18
|
"demo.frontend.actions": "常用操作",
|
|
68
19
|
"demo.server.title": "NocoBase 元数据翻译",
|
|
69
20
|
"demo.server.description":
|
|
@@ -79,7 +30,6 @@ const zhCN = {
|
|
|
79
30
|
"demo.prompt.copyError": "无法访问剪贴板,请手动选中并复制提示词。",
|
|
80
31
|
"demo.prompt.output": "生成的提示词",
|
|
81
32
|
"demo.prompt.outputDescription": "修改场景和语言要求时会实时更新。",
|
|
82
|
-
},
|
|
83
33
|
} as const;
|
|
84
34
|
|
|
85
35
|
export default zhCN;
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { useEffect, useState, type PropsWithChildren } from "react";
|
|
2
2
|
|
|
3
3
|
import { LoadingState } from "@/components/app-shell/loading-state";
|
|
4
|
-
import {
|
|
4
|
+
import { nocobaseClient } from "@/lib/nocobase/client";
|
|
5
|
+
import { setLocalePersistence } from "@/providers/i18n";
|
|
6
|
+
import { useSystemSettings } from "@/providers/system-settings";
|
|
5
7
|
import { loadServerLocaleResources } from "./server-resources";
|
|
6
|
-
import { translate } from "./runtime";
|
|
7
8
|
|
|
8
|
-
export function
|
|
9
|
+
export function NocoBaseI18nBootstrap({ children }: PropsWithChildren) {
|
|
10
|
+
const { settings } = useSystemSettings();
|
|
9
11
|
const [ready, setReady] = useState(false);
|
|
10
12
|
|
|
11
|
-
useEffect(
|
|
13
|
+
useEffect(
|
|
14
|
+
() =>
|
|
15
|
+
setLocalePersistence(async (locale) => {
|
|
16
|
+
if (!nocobaseClient.getToken()) return;
|
|
17
|
+
await nocobaseClient.action("users", "updateLang", {
|
|
18
|
+
method: "POST",
|
|
19
|
+
body: { appLang: locale },
|
|
20
|
+
});
|
|
21
|
+
}),
|
|
22
|
+
[]
|
|
23
|
+
);
|
|
12
24
|
|
|
13
25
|
useEffect(() => {
|
|
14
26
|
let active = true;
|
|
15
|
-
void loadServerLocaleResources()
|
|
27
|
+
void loadServerLocaleResources(settings)
|
|
16
28
|
.catch((error) => {
|
|
17
29
|
console.warn("Unable to load NocoBase locale resources", error);
|
|
18
30
|
})
|
|
@@ -22,7 +34,7 @@ export function LocalePreferenceProvider({ children }: PropsWithChildren) {
|
|
|
22
34
|
return () => {
|
|
23
35
|
active = false;
|
|
24
36
|
};
|
|
25
|
-
}, []);
|
|
37
|
+
}, [settings]);
|
|
26
38
|
|
|
27
39
|
if (!ready) {
|
|
28
40
|
return <LoadingState className="min-h-80" />;
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { nocobaseClient } from "@/lib/nocobase/client";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
type LocaleSystemSettings,
|
|
4
|
+
applyDocumentLocale,
|
|
5
|
+
getCurrentLocale,
|
|
6
|
+
i18n,
|
|
4
7
|
registerLocale,
|
|
5
8
|
setEnabledLocales,
|
|
6
|
-
} from "
|
|
7
|
-
import { applyDocumentLocale, getCurrentLocale, i18n } from "./runtime";
|
|
9
|
+
} from "@/providers/i18n";
|
|
8
10
|
|
|
9
11
|
type ServerLanguagePayload = {
|
|
10
12
|
lang?: string;
|
|
11
13
|
resources?: Record<string, Record<string, string>>;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
|
-
type SystemSettingsPayload = {
|
|
15
|
-
enabledLanguages?: string[];
|
|
16
|
-
};
|
|
17
|
-
|
|
18
16
|
const serverResourceNamespaces = new Set(["lm-collections"]);
|
|
19
17
|
const loadedServerResourceNamespaces = new Set<string>();
|
|
20
18
|
const namespaceLoadPromises = new Map<string, Promise<void>>();
|
|
@@ -119,37 +117,23 @@ export function getServerResourceNamespaces() {
|
|
|
119
117
|
return [...serverResourceNamespaces];
|
|
120
118
|
}
|
|
121
119
|
|
|
122
|
-
export function loadServerLocaleResources() {
|
|
120
|
+
export function loadServerLocaleResources(settings?: LocaleSystemSettings) {
|
|
123
121
|
if (serverResourcesPromise) return serverResourcesPromise;
|
|
124
122
|
|
|
125
123
|
bootstrapStarted = true;
|
|
126
124
|
const requestedLocale = nocobaseClient.getStoredLocale();
|
|
127
125
|
const initialNamespaces = [...serverResourceNamespaces];
|
|
128
|
-
const
|
|
129
|
-
.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
withAclMeta: false,
|
|
133
|
-
})
|
|
134
|
-
.catch((error) => {
|
|
135
|
-
console.warn("Unable to load enabled NocoBase languages", error);
|
|
136
|
-
return undefined;
|
|
137
|
-
})
|
|
138
|
-
.then((systemSettings) => {
|
|
139
|
-
const configuredLocales = Array.isArray(systemSettings?.enabledLanguages)
|
|
140
|
-
? systemSettings.enabledLanguages
|
|
141
|
-
: [];
|
|
142
|
-
if (configuredLocales.length) setEnabledLocales(configuredLocales);
|
|
143
|
-
return systemSettings;
|
|
144
|
-
});
|
|
126
|
+
const configuredLocales = Array.isArray(settings?.enabledLanguages)
|
|
127
|
+
? settings.enabledLanguages
|
|
128
|
+
: [];
|
|
129
|
+
if (configuredLocales.length) setEnabledLocales(configuredLocales);
|
|
145
130
|
const languagePromise = requestServerResources(
|
|
146
131
|
initialNamespaces,
|
|
147
132
|
requestedLocale
|
|
148
133
|
);
|
|
149
134
|
|
|
150
|
-
serverResourcesPromise =
|
|
151
|
-
.then(async (
|
|
152
|
-
const configuredLocales = systemSettings?.enabledLanguages ?? [];
|
|
135
|
+
serverResourcesPromise = languagePromise
|
|
136
|
+
.then(async (payload) => {
|
|
153
137
|
const serverLocale = payload.lang;
|
|
154
138
|
|
|
155
139
|
if (!configuredLocales.length && serverLocale) {
|
|
@@ -173,9 +157,7 @@ export function loadServerLocaleResources() {
|
|
|
173
157
|
})
|
|
174
158
|
.catch((error) => {
|
|
175
159
|
bootstrapComplete = true;
|
|
176
|
-
if (!
|
|
177
|
-
setEnabledLocales([getCurrentLocale()]);
|
|
178
|
-
}
|
|
160
|
+
if (!configuredLocales.length) setEnabledLocales([getCurrentLocale()]);
|
|
179
161
|
serverResourcesPromise = undefined;
|
|
180
162
|
throw error;
|
|
181
163
|
});
|
|
@@ -9,13 +9,16 @@ const server = await createServer({
|
|
|
9
9
|
|
|
10
10
|
try {
|
|
11
11
|
await server.ssrLoadModule("/src/locales/index.ts");
|
|
12
|
-
const {
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const {
|
|
13
|
+
applySystemLocale,
|
|
14
|
+
getCurrentLocale,
|
|
15
|
+
i18n,
|
|
16
|
+
registerLocaleResources,
|
|
17
|
+
translate,
|
|
18
|
+
} = await server.ssrLoadModule("/src/providers/i18n/runtime.ts");
|
|
19
|
+
const { resolveTranslatableText } =
|
|
15
20
|
await server.ssrLoadModule("/src/lib/i18n.ts");
|
|
16
21
|
|
|
17
|
-
setTranslationResolver(translate);
|
|
18
|
-
|
|
19
22
|
await i18n.changeLanguage("en-US");
|
|
20
23
|
assert.equal(translate("resources.users", { ns: "app" }, "Users"), "Users");
|
|
21
24
|
|
|
@@ -36,6 +39,12 @@ try {
|
|
|
36
39
|
"示例"
|
|
37
40
|
);
|
|
38
41
|
|
|
42
|
+
await applySystemLocale({
|
|
43
|
+
appLang: "en-US",
|
|
44
|
+
enabledLanguages: ["en-US", "zh-CN"],
|
|
45
|
+
});
|
|
46
|
+
assert.equal(getCurrentLocale(), "en-US");
|
|
47
|
+
|
|
39
48
|
console.log("NocoBase i18n regression tests passed");
|
|
40
49
|
} finally {
|
|
41
50
|
await server.close();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Route surfaces
|
|
2
|
+
|
|
3
|
+
Route surfaces keep URL navigation separate from visual presentation. The same
|
|
4
|
+
business content can be hosted by a routed drawer, dialog, or full page.
|
|
5
|
+
|
|
6
|
+
The Registry also installs a lazy-loaded Demo route at `/route-surfaces` with
|
|
7
|
+
live drawer, dialog, child-page, nested-drawer, and mixed page/drawer/dialog
|
|
8
|
+
scenarios plus a Prompt generator.
|
|
9
|
+
|
|
10
|
+
## Components
|
|
11
|
+
|
|
12
|
+
- `RouteDrawer` supports URL-backed nested drawers and push-style stacking.
|
|
13
|
+
- `RouteDialog` provides the same close contract for modal routes.
|
|
14
|
+
- `RoutePage` provides the close context without an overlay.
|
|
15
|
+
- `useRouteSurfaceClose` lets content request a close without knowing how it is
|
|
16
|
+
presented.
|
|
17
|
+
- `useRefineUnsavedChangesGuard` adapts Refine's unsaved-change state to the
|
|
18
|
+
route surface close lifecycle and renders a shadcn Alert Dialog confirmation.
|
|
19
|
+
|
|
20
|
+
Route definitions, ACL guards, resource data fetching, and application-specific
|
|
21
|
+
paths remain application concerns.
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { ArrowLeft, ArrowRight, Layers3 } from "lucide-react";
|
|
2
|
+
import { useOutlet, useNavigate } from "react-router";
|
|
3
|
+
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
7
|
+
import {
|
|
8
|
+
RouteDialog,
|
|
9
|
+
RouteDrawer,
|
|
10
|
+
RoutePage,
|
|
11
|
+
useRouteSurfaceClose,
|
|
12
|
+
} from "../index";
|
|
13
|
+
import { RouteSurfacePromptGenerator } from "./prompt-generator";
|
|
14
|
+
import { routeSurfaceScenarios } from "./scenarios";
|
|
15
|
+
|
|
16
|
+
const demoBase = "/route-surfaces";
|
|
17
|
+
|
|
18
|
+
export function RouteSurfacesDemoHome() {
|
|
19
|
+
const navigate = useNavigate();
|
|
20
|
+
const overlay = useOutlet();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<div className="space-y-6">
|
|
25
|
+
<div className="space-y-2">
|
|
26
|
+
<Badge variant="secondary">Route surfaces</Badge>
|
|
27
|
+
<h1 className="font-heading text-3xl font-semibold tracking-tight">
|
|
28
|
+
URL-backed pages and overlays
|
|
29
|
+
</h1>
|
|
30
|
+
<p className="max-w-3xl text-muted-foreground">
|
|
31
|
+
Keep business content independent from whether it appears as a page,
|
|
32
|
+
drawer, dialog, or nested combination. Every preview below supports a
|
|
33
|
+
direct URL and browser history.
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
|
|
38
|
+
{routeSurfaceScenarios.map((scenario) => (
|
|
39
|
+
<Card key={scenario.id} className="flex flex-col">
|
|
40
|
+
<CardHeader>
|
|
41
|
+
<div className="flex items-center justify-between gap-3">
|
|
42
|
+
<Badge variant="outline">Scenario {scenario.number}</Badge>
|
|
43
|
+
<span className="text-xs text-muted-foreground">
|
|
44
|
+
{scenario.routeShape}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
<CardTitle>{scenario.title}</CardTitle>
|
|
48
|
+
<CardDescription>{scenario.description}</CardDescription>
|
|
49
|
+
</CardHeader>
|
|
50
|
+
<CardContent className="mt-auto">
|
|
51
|
+
<Button
|
|
52
|
+
type="button"
|
|
53
|
+
variant="outline"
|
|
54
|
+
className="w-full"
|
|
55
|
+
onClick={() => navigate(scenario.path)}
|
|
56
|
+
>
|
|
57
|
+
Open preview
|
|
58
|
+
<ArrowRight />
|
|
59
|
+
</Button>
|
|
60
|
+
</CardContent>
|
|
61
|
+
</Card>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<RouteSurfacePromptGenerator />
|
|
66
|
+
</div>
|
|
67
|
+
{overlay}
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function DemoDrawerRoute() {
|
|
73
|
+
const navigate = useNavigate();
|
|
74
|
+
const nested = useOutlet();
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<RouteDrawer
|
|
78
|
+
title="Customer details"
|
|
79
|
+
description="A routed drawer keeps the scenario page mounted behind it."
|
|
80
|
+
closeLabel="Close"
|
|
81
|
+
closeTo={demoBase}
|
|
82
|
+
nested={nested}
|
|
83
|
+
>
|
|
84
|
+
<DemoSurfaceBody
|
|
85
|
+
label="Drawer"
|
|
86
|
+
title="Northwind renewal"
|
|
87
|
+
description="Business content does not know that it is rendered inside a drawer."
|
|
88
|
+
>
|
|
89
|
+
<Button onClick={() => navigate("second")}>
|
|
90
|
+
Open second-level drawer
|
|
91
|
+
<ArrowRight />
|
|
92
|
+
</Button>
|
|
93
|
+
</DemoSurfaceBody>
|
|
94
|
+
</RouteDrawer>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function DemoSecondDrawerRoute() {
|
|
99
|
+
return (
|
|
100
|
+
<RouteDrawer
|
|
101
|
+
title="Renewal activity"
|
|
102
|
+
description="The lower drawer is pushed outward and remains behind the layer mask."
|
|
103
|
+
closeLabel="Close"
|
|
104
|
+
closeTo={`${demoBase}/drawer`}
|
|
105
|
+
>
|
|
106
|
+
<DemoSurfaceBody
|
|
107
|
+
label="Second-level drawer"
|
|
108
|
+
title="Latest review"
|
|
109
|
+
description="Clicking this layer's backdrop closes only the top drawer."
|
|
110
|
+
/>
|
|
111
|
+
</RouteDrawer>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function DemoDialogRoute() {
|
|
116
|
+
return (
|
|
117
|
+
<RouteDialog
|
|
118
|
+
title="Approve renewal"
|
|
119
|
+
description="A modal route for a focused decision."
|
|
120
|
+
closeLabel="Close"
|
|
121
|
+
closeTo={demoBase}
|
|
122
|
+
>
|
|
123
|
+
<DemoSurfaceBody
|
|
124
|
+
label="Dialog"
|
|
125
|
+
title="Approval required"
|
|
126
|
+
description="The page stays mounted while the URL represents the active dialog."
|
|
127
|
+
/>
|
|
128
|
+
</RouteDialog>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function DemoPageRoute() {
|
|
133
|
+
const nested = useOutlet();
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<RoutePage closeTo={demoBase}>
|
|
137
|
+
<DemoPageContent nested={nested} />
|
|
138
|
+
</RoutePage>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function DemoPageContent({ nested }: { nested: React.ReactNode }) {
|
|
143
|
+
const close = useRouteSurfaceClose();
|
|
144
|
+
const navigate = useNavigate();
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<>
|
|
148
|
+
<div className="space-y-6">
|
|
149
|
+
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
150
|
+
<div className="space-y-2">
|
|
151
|
+
<Badge variant="secondary">Child page</Badge>
|
|
152
|
+
<h1 className="font-heading text-3xl font-semibold tracking-tight">
|
|
153
|
+
Customer workspace
|
|
154
|
+
</h1>
|
|
155
|
+
<p className="text-muted-foreground">
|
|
156
|
+
This route replaces the Demo home instead of rendering inside its Outlet.
|
|
157
|
+
</p>
|
|
158
|
+
</div>
|
|
159
|
+
<Button variant="outline" onClick={() => void close()}>
|
|
160
|
+
<ArrowLeft />
|
|
161
|
+
Back to scenarios
|
|
162
|
+
</Button>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<Card>
|
|
166
|
+
<CardHeader>
|
|
167
|
+
<div className="flex items-center gap-2">
|
|
168
|
+
<Layers3 className="size-5 text-primary" />
|
|
169
|
+
<CardTitle>Page-owned workflow</CardTitle>
|
|
170
|
+
</div>
|
|
171
|
+
<CardDescription>
|
|
172
|
+
A page can still host routed overlays without becoming coupled to their content.
|
|
173
|
+
</CardDescription>
|
|
174
|
+
</CardHeader>
|
|
175
|
+
<CardContent>
|
|
176
|
+
<Button onClick={() => navigate("drawer")}>Open page drawer</Button>
|
|
177
|
+
</CardContent>
|
|
178
|
+
</Card>
|
|
179
|
+
</div>
|
|
180
|
+
{nested}
|
|
181
|
+
</>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function DemoPageDrawerRoute() {
|
|
186
|
+
const navigate = useNavigate();
|
|
187
|
+
const nested = useOutlet();
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<RouteDrawer
|
|
191
|
+
title="Customer activity"
|
|
192
|
+
description="This drawer belongs to the child page route."
|
|
193
|
+
closeLabel="Close"
|
|
194
|
+
closeTo={`${demoBase}/page`}
|
|
195
|
+
nested={nested}
|
|
196
|
+
>
|
|
197
|
+
<DemoSurfaceBody
|
|
198
|
+
label="Page drawer"
|
|
199
|
+
title="Renewal conversation"
|
|
200
|
+
description="Open a dialog above this drawer to test mixed presentation types."
|
|
201
|
+
>
|
|
202
|
+
<Button onClick={() => navigate("dialog")}>
|
|
203
|
+
Open confirmation dialog
|
|
204
|
+
<ArrowRight />
|
|
205
|
+
</Button>
|
|
206
|
+
</DemoSurfaceBody>
|
|
207
|
+
</RouteDrawer>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function DemoPageDrawerDialogRoute() {
|
|
212
|
+
return (
|
|
213
|
+
<RouteDialog
|
|
214
|
+
title="Confirm follow-up"
|
|
215
|
+
description="The dialog is nested in the drawer route subtree."
|
|
216
|
+
closeLabel="Close"
|
|
217
|
+
closeTo={`${demoBase}/page/drawer`}
|
|
218
|
+
>
|
|
219
|
+
<DemoSurfaceBody
|
|
220
|
+
label="Nested dialog"
|
|
221
|
+
title="Schedule a follow-up?"
|
|
222
|
+
description="Closing returns to the drawer; closing the drawer then returns to the page."
|
|
223
|
+
/>
|
|
224
|
+
</RouteDialog>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function DemoSurfaceBody({
|
|
229
|
+
label,
|
|
230
|
+
title,
|
|
231
|
+
description,
|
|
232
|
+
children,
|
|
233
|
+
}: {
|
|
234
|
+
label: string;
|
|
235
|
+
title: string;
|
|
236
|
+
description: string;
|
|
237
|
+
children?: React.ReactNode;
|
|
238
|
+
}) {
|
|
239
|
+
return (
|
|
240
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-5">
|
|
241
|
+
<div className="space-y-5">
|
|
242
|
+
<Badge variant="outline">{label}</Badge>
|
|
243
|
+
<div className="space-y-2">
|
|
244
|
+
<h2 className="text-xl font-semibold">{title}</h2>
|
|
245
|
+
<p className="text-muted-foreground">{description}</p>
|
|
246
|
+
</div>
|
|
247
|
+
<div className="grid gap-3 rounded-lg border bg-muted/30 p-4 sm:grid-cols-2">
|
|
248
|
+
<DemoField label="Owner" value="Ada Lovelace" />
|
|
249
|
+
<DemoField label="Status" value="In review" />
|
|
250
|
+
<DemoField label="Value" value="$48,000" />
|
|
251
|
+
<DemoField label="Renewal" value="September 30" />
|
|
252
|
+
</div>
|
|
253
|
+
{children ? <div>{children}</div> : null}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function DemoField({ label, value }: { label: string; value: string }) {
|
|
260
|
+
return (
|
|
261
|
+
<div className="space-y-1">
|
|
262
|
+
<div className="text-xs text-muted-foreground">{label}</div>
|
|
263
|
+
<div className="font-medium">{value}</div>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
package/{src/extensions/nocobase-acl → registry/nocobase-route-surfaces}/demo/lazy-route.tsx
RENAMED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Suspense, type ReactNode } from "react";
|
|
2
|
+
|
|
2
3
|
import { LoadingState } from "@/components/app-shell/loading-state";
|
|
3
4
|
|
|
4
|
-
export function
|
|
5
|
+
export function LazyRouteSurfaceDemo({ children }: { children: ReactNode }) {
|
|
5
6
|
return (
|
|
6
7
|
<Suspense fallback={<LoadingState className="min-h-80" />}>
|
|
7
8
|
{children}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Check, Copy } from "lucide-react";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import { Label } from "@/components/ui/label";
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from "@/components/ui/select";
|
|
15
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
16
|
+
import {
|
|
17
|
+
getRouteSurfacePrompt,
|
|
18
|
+
routeSurfaceScenarios,
|
|
19
|
+
type RouteSurfaceScenarioId,
|
|
20
|
+
} from "./scenarios";
|
|
21
|
+
|
|
22
|
+
export function RouteSurfacePromptGenerator() {
|
|
23
|
+
const [scenarioId, setScenarioId] =
|
|
24
|
+
useState<RouteSurfaceScenarioId>("drawer");
|
|
25
|
+
const [target, setTarget] = useState("a customer detail workflow");
|
|
26
|
+
const [copied, setCopied] = useState(false);
|
|
27
|
+
const scenario =
|
|
28
|
+
routeSurfaceScenarios.find((item) => item.id === scenarioId) ??
|
|
29
|
+
routeSurfaceScenarios[0];
|
|
30
|
+
const prompt = useMemo(
|
|
31
|
+
() => getRouteSurfacePrompt(scenario, target),
|
|
32
|
+
[scenario, target]
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const copyPrompt = async () => {
|
|
36
|
+
await navigator.clipboard.writeText(prompt);
|
|
37
|
+
setCopied(true);
|
|
38
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Card>
|
|
43
|
+
<CardHeader>
|
|
44
|
+
<CardTitle>Prompt generator</CardTitle>
|
|
45
|
+
<CardDescription>
|
|
46
|
+
Generate a complete routing scenario, not just an isolated overlay component.
|
|
47
|
+
</CardDescription>
|
|
48
|
+
</CardHeader>
|
|
49
|
+
<CardContent className="space-y-4">
|
|
50
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
51
|
+
<div className="space-y-2">
|
|
52
|
+
<Label htmlFor="route-surface-scenario">Scenario</Label>
|
|
53
|
+
<Select
|
|
54
|
+
value={scenarioId}
|
|
55
|
+
onValueChange={(value) =>
|
|
56
|
+
setScenarioId(value as RouteSurfaceScenarioId)
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
<SelectTrigger id="route-surface-scenario" className="w-full">
|
|
60
|
+
<SelectValue />
|
|
61
|
+
</SelectTrigger>
|
|
62
|
+
<SelectContent>
|
|
63
|
+
{routeSurfaceScenarios.map((item) => (
|
|
64
|
+
<SelectItem key={item.id} value={item.id}>
|
|
65
|
+
{item.number}. {item.title}
|
|
66
|
+
</SelectItem>
|
|
67
|
+
))}
|
|
68
|
+
</SelectContent>
|
|
69
|
+
</Select>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="space-y-2">
|
|
72
|
+
<Label htmlFor="route-surface-target">Business target</Label>
|
|
73
|
+
<Input
|
|
74
|
+
id="route-surface-target"
|
|
75
|
+
value={target}
|
|
76
|
+
onChange={(event) => setTarget(event.target.value)}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<Textarea value={prompt} readOnly className="min-h-80 font-mono text-xs" />
|
|
81
|
+
<div className="flex justify-end">
|
|
82
|
+
<Button type="button" onClick={() => void copyPrompt()}>
|
|
83
|
+
{copied ? <Check /> : <Copy />}
|
|
84
|
+
{copied ? "Copied" : "Copy prompt"}
|
|
85
|
+
</Button>
|
|
86
|
+
</div>
|
|
87
|
+
</CardContent>
|
|
88
|
+
</Card>
|
|
89
|
+
);
|
|
90
|
+
}
|