@nocobase/portal-template-default 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.MD +22 -3
  2. package/package.json +4 -2
  3. package/registry/nocobase-acl/components/role-options.ts +7 -0
  4. package/registry/nocobase-acl/components/role-switcher.tsx +9 -4
  5. package/registry/nocobase-acl/demo/boundary-api.tsx +87 -16
  6. package/registry/nocobase-acl/demo/components.tsx +32 -11
  7. package/registry/nocobase-acl/demo/index.tsx +45 -14
  8. package/registry/nocobase-acl/demo/lazy-route.tsx +2 -7
  9. package/registry/nocobase-acl/demo/prompt-generator.tsx +52 -33
  10. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +28 -9
  11. package/registry/nocobase-acl/extension.tsx +1 -1
  12. package/registry/nocobase-acl/tests/acl-regression.mjs +7 -6
  13. package/registry/nocobase-ai/components/chat/chat-messages.tsx +3 -4
  14. package/registry/nocobase-ai/components/chat/conversation-list.tsx +2 -4
  15. package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +16 -19
  16. package/registry/nocobase-ai/components/tools/chart-renderer.tsx +3 -14
  17. package/registry/nocobase-ai/components/tools/echarts-preview.tsx +2 -5
  18. package/registry/nocobase-ai/demo/configuration-gate.tsx +9 -12
  19. package/registry/nocobase-ai/extension.tsx +2 -7
  20. package/registry/nocobase-i18n/README.md +53 -0
  21. package/registry/nocobase-i18n/components/index.ts +1 -0
  22. package/registry/nocobase-i18n/components/language-switcher.tsx +138 -0
  23. package/registry/nocobase-i18n/demo/index.tsx +209 -0
  24. package/registry/nocobase-i18n/demo/prompt-generator.tsx +155 -0
  25. package/registry/nocobase-i18n/extension.tsx +46 -0
  26. package/registry/nocobase-i18n/i18n-provider.ts +9 -0
  27. package/registry/nocobase-i18n/index.ts +6 -0
  28. package/registry/nocobase-i18n/locale-store.ts +134 -0
  29. package/registry/nocobase-i18n/locales/en-US.ts +89 -0
  30. package/registry/nocobase-i18n/locales/zh-CN.ts +85 -0
  31. package/registry/nocobase-i18n/provider.tsx +32 -0
  32. package/registry/nocobase-i18n/runtime.ts +118 -0
  33. package/registry/nocobase-i18n/server-resources.ts +184 -0
  34. package/registry/nocobase-i18n/tests/i18n-regression.mjs +42 -0
  35. package/registry.config.json +34 -14
  36. package/scripts/registry.mjs +5 -0
  37. package/src/App.tsx +27 -86
  38. package/src/app/extension.ts +3 -1
  39. package/src/app/extensions.tsx +21 -0
  40. package/src/components/access-control/acl-gate.tsx +4 -10
  41. package/src/components/app-shell/breadcrumb.tsx +39 -3
  42. package/src/components/app-shell/document-title-handler.tsx +11 -20
  43. package/src/components/app-shell/header.tsx +18 -3
  44. package/src/components/app-shell/loading-overlay.tsx +2 -4
  45. package/src/components/app-shell/loading-state.tsx +15 -0
  46. package/src/components/app-shell/sidebar.tsx +75 -47
  47. package/src/components/data-table/data-table-pagination.tsx +27 -7
  48. package/src/components/data-table/data-table-styles.ts +57 -0
  49. package/src/components/data-table/data-table.tsx +14 -59
  50. package/src/components/resources/resource-label.ts +101 -0
  51. package/src/components/resources/views/create-view.tsx +17 -14
  52. package/src/components/resources/views/edit-view.tsx +17 -15
  53. package/src/components/resources/views/list-view.tsx +20 -10
  54. package/src/components/resources/views/show-view.tsx +20 -18
  55. package/src/extensions/nocobase-acl/README.md +19 -0
  56. package/src/extensions/nocobase-acl/components/acl-boundary.tsx +106 -0
  57. package/src/extensions/nocobase-acl/components/index.ts +3 -0
  58. package/src/extensions/nocobase-acl/components/role-options.ts +36 -0
  59. package/src/extensions/nocobase-acl/components/role-switcher.tsx +135 -0
  60. package/src/extensions/nocobase-acl/demo/boundary-api.tsx +186 -0
  61. package/src/extensions/nocobase-acl/demo/components.tsx +231 -0
  62. package/src/extensions/nocobase-acl/demo/index.tsx +505 -0
  63. package/src/extensions/nocobase-acl/demo/lazy-route.tsx +10 -0
  64. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +134 -0
  65. package/src/extensions/nocobase-acl/demo/role-switcher-prompt-generator.tsx +198 -0
  66. package/src/extensions/nocobase-acl/demo/scenario-section.tsx +37 -0
  67. package/src/extensions/nocobase-acl/extension.tsx +71 -0
  68. package/src/extensions/nocobase-acl/index.ts +1 -0
  69. package/src/extensions/nocobase-ai/README.md +70 -0
  70. package/src/extensions/nocobase-ai/adapters/react-hook-form.ts +20 -0
  71. package/src/extensions/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
  72. package/src/extensions/nocobase-ai/components/ai-root-provider.tsx +32 -0
  73. package/src/extensions/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
  74. package/src/extensions/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
  75. package/src/extensions/nocobase-ai/components/chat/chat-compact.tsx +90 -0
  76. package/src/extensions/nocobase-ai/components/chat/chat-composer.tsx +429 -0
  77. package/src/extensions/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
  78. package/src/extensions/nocobase-ai/components/chat/chat-header.tsx +88 -0
  79. package/src/extensions/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
  80. package/src/extensions/nocobase-ai/components/chat/chat-message.tsx +252 -0
  81. package/src/extensions/nocobase-ai/components/chat/chat-messages.tsx +173 -0
  82. package/src/extensions/nocobase-ai/components/chat/chat-window.tsx +118 -0
  83. package/src/extensions/nocobase-ai/components/chat/conversation-list.tsx +343 -0
  84. package/src/extensions/nocobase-ai/components/chat/markdown-message.tsx +165 -0
  85. package/src/extensions/nocobase-ai/components/chat/model-select-options.tsx +30 -0
  86. package/src/extensions/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
  87. package/src/extensions/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
  88. package/src/extensions/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
  89. package/src/extensions/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
  90. package/src/extensions/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
  91. package/src/extensions/nocobase-ai/components/index.ts +59 -0
  92. package/src/extensions/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
  93. package/src/extensions/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
  94. package/src/extensions/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
  95. package/src/extensions/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
  96. package/src/extensions/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
  97. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
  98. package/src/extensions/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
  99. package/src/extensions/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
  100. package/src/extensions/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
  101. package/src/extensions/nocobase-ai/components/tools/business-report-dialog.tsx +364 -0
  102. package/src/extensions/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
  103. package/src/extensions/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
  104. package/src/extensions/nocobase-ai/components/tools/chart-renderer.tsx +99 -0
  105. package/src/extensions/nocobase-ai/components/tools/echarts-preview.tsx +59 -0
  106. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
  107. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
  108. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
  109. package/src/extensions/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
  110. package/src/extensions/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
  111. package/src/extensions/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
  112. package/src/extensions/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
  113. package/src/extensions/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
  114. package/src/extensions/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
  115. package/src/extensions/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
  116. package/src/extensions/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
  117. package/src/extensions/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
  118. package/src/extensions/nocobase-ai/demo/configuration-gate.tsx +49 -0
  119. package/src/extensions/nocobase-ai/demo/container-showcase.tsx +210 -0
  120. package/src/extensions/nocobase-ai/demo/floating.tsx +134 -0
  121. package/src/extensions/nocobase-ai/demo/index.tsx +346 -0
  122. package/src/extensions/nocobase-ai/demo/interaction-showcase.tsx +151 -0
  123. package/src/extensions/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
  124. package/src/extensions/nocobase-ai/demo/page-context.tsx +884 -0
  125. package/src/extensions/nocobase-ai/demo/page-element-showcase.tsx +197 -0
  126. package/src/extensions/nocobase-ai/demo/prompt-card.tsx +41 -0
  127. package/src/extensions/nocobase-ai/demo/prompt-generator.tsx +452 -0
  128. package/src/extensions/nocobase-ai/demo/shortcut.tsx +1207 -0
  129. package/src/extensions/nocobase-ai/demo/tool-cards.tsx +530 -0
  130. package/src/extensions/nocobase-ai/extension.tsx +158 -0
  131. package/src/extensions/nocobase-ai/global-ai-chat.tsx +213 -0
  132. package/src/extensions/nocobase-ai/index.ts +3 -0
  133. package/src/extensions/nocobase-ai/providers/ai-provider.tsx +323 -0
  134. package/src/extensions/nocobase-ai/providers/avatars.ts +817 -0
  135. package/src/extensions/nocobase-ai/providers/chat-context.tsx +109 -0
  136. package/src/extensions/nocobase-ai/providers/chat-controller.ts +84 -0
  137. package/src/extensions/nocobase-ai/providers/chat-message-utils.ts +198 -0
  138. package/src/extensions/nocobase-ai/providers/chat-provider.tsx +871 -0
  139. package/src/extensions/nocobase-ai/providers/chat-reducer.ts +147 -0
  140. package/src/extensions/nocobase-ai/providers/chat-task-utils.ts +105 -0
  141. package/src/extensions/nocobase-ai/providers/chat-transport.ts +184 -0
  142. package/src/extensions/nocobase-ai/providers/form-registry.tsx +271 -0
  143. package/src/extensions/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
  144. package/src/extensions/nocobase-ai/providers/index.ts +61 -0
  145. package/src/extensions/nocobase-ai/providers/model.ts +32 -0
  146. package/src/extensions/nocobase-ai/providers/page-context.tsx +136 -0
  147. package/src/extensions/nocobase-ai/providers/stream-coalescer.ts +60 -0
  148. package/src/extensions/nocobase-ai/providers/stream-event-utils.ts +82 -0
  149. package/src/extensions/nocobase-ai/providers/stream-parser.ts +61 -0
  150. package/src/extensions/nocobase-ai/providers/sub-agent-stream.ts +316 -0
  151. package/src/extensions/nocobase-ai/providers/types.ts +183 -0
  152. package/src/extensions/nocobase-ai/providers/ui-message-stream.ts +463 -0
  153. package/src/extensions/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
  154. package/src/extensions/nocobase-ai/providers/use-chat-attachments.ts +206 -0
  155. package/src/extensions/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
  156. package/src/extensions/nocobase-ai/providers/use-chat-runtime.ts +210 -0
  157. package/src/extensions/nocobase-ai/providers/use-chat-work-context.ts +93 -0
  158. package/src/extensions/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
  159. package/src/extensions/nocobase-ai/providers/use-conversation-history.ts +178 -0
  160. package/src/extensions/nocobase-ai/services/index.ts +7 -0
  161. package/src/extensions/nocobase-ai/services/nocobase-ai-service.ts +454 -0
  162. package/src/extensions/nocobase-ai/services/types.ts +82 -0
  163. package/src/index.tsx +2 -3
  164. package/src/lib/i18n.ts +161 -0
  165. package/src/lib/nocobase/client.ts +19 -2
  166. package/src/locales/en-US.ts +82 -0
  167. package/src/locales/index.ts +13 -0
  168. package/src/locales/zh-CN.ts +79 -0
  169. package/src/pages/users/create.tsx +98 -0
  170. package/src/pages/users/edit.tsx +92 -0
  171. package/src/pages/users/form-context.ts +57 -0
  172. package/src/pages/users/form-fields.tsx +211 -0
  173. package/src/pages/users/list.tsx +205 -0
  174. package/src/pages/users/show.tsx +187 -0
  175. package/src/pages/users/types.ts +20 -0
  176. package/src/providers/constants.ts +1 -0
  177. package/src/pages/blog-posts/create.tsx +0 -177
  178. package/src/pages/blog-posts/edit.tsx +0 -197
  179. package/src/pages/blog-posts/list.tsx +0 -131
  180. package/src/pages/blog-posts/show.tsx +0 -81
  181. package/src/pages/categories/create.tsx +0 -73
  182. package/src/pages/categories/edit.tsx +0 -73
  183. package/src/pages/categories/index.ts +0 -4
  184. package/src/pages/categories/list.tsx +0 -86
  185. package/src/pages/categories/show.tsx +0 -37
  186. /package/src/pages/{blog-posts → users}/index.ts +0 -0
@@ -1,16 +1,13 @@
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 { RefreshButton } from "@/components/resources/buttons/refresh";
13
9
  import { ArrowLeftIcon } from "lucide-react";
10
+ import { useResourceLabel } from "@/components/resources/resource-label";
14
11
 
15
12
  type EditViewProps = PropsWithChildren<{
16
13
  className?: string;
@@ -41,8 +38,7 @@ export const EditViewHeader = ({
41
38
  headerClassName,
42
39
  }: EditViewHeaderProps) => {
43
40
  const back = useBack();
44
-
45
- const getUserFriendlyName = useUserFriendlyName();
41
+ const translate = useTranslate();
46
42
 
47
43
  const { resource, identifier } = useResourceParams({
48
44
  resource: resourceFromProps,
@@ -51,11 +47,19 @@ export const EditViewHeader = ({
51
47
 
52
48
  const resourceName = resource?.name ?? identifier;
53
49
 
54
- const resourceTitle = getUserFriendlyName(
55
- resource?.meta?.label ?? identifier ?? resource?.name,
56
- "singular"
50
+ const resourceTitle = useResourceLabel(resource, "singular", identifier);
51
+ const title =
52
+ titleFromProps ??
53
+ translate(
54
+ "views.edit.title",
55
+ { resource: resourceTitle },
56
+ `Edit ${resourceTitle}`
57
+ );
58
+ const description = translate(
59
+ "views.edit.description",
60
+ { resource: resourceTitle.toLocaleLowerCase() },
61
+ `Update this ${resourceTitle.toLocaleLowerCase()} while NocoBase keeps the data consistent.`
57
62
  );
58
- const title = titleFromProps ?? `Edit ${resourceTitle}`;
59
63
 
60
64
  return (
61
65
  <div className={cn("flex flex-col", "gap-3", wrapperClassName)}>
@@ -74,6 +78,7 @@ export const EditViewHeader = ({
74
78
  size="icon"
75
79
  className="mt-0.5 rounded-lg"
76
80
  onClick={back}
81
+ aria-label={translate("buttons.cancel", "Cancel")}
77
82
  >
78
83
  <ArrowLeftIcon className="h-4 w-4" />
79
84
  </Button>
@@ -81,10 +86,7 @@ export const EditViewHeader = ({
81
86
  <h2 className="text-3xl font-semibold tracking-[-0.035em]">
82
87
  {title}
83
88
  </h2>
84
- <p className="mt-2 text-sm text-muted-foreground">
85
- Update this {resourceTitle.toLowerCase()} while NocoBase keeps
86
- the data consistent.
87
- </p>
89
+ <p className="mt-2 text-sm text-muted-foreground">{description}</p>
88
90
  </div>
89
91
  </div>
90
92
 
@@ -2,9 +2,11 @@
2
2
 
3
3
  import type { PropsWithChildren } from "react";
4
4
 
5
- import { useResourceParams, useUserFriendlyName } from "@refinedev/core";
5
+ import { useResourceParams, useTranslate } from "@refinedev/core";
6
6
  import { Breadcrumb } from "@/components/app-shell/breadcrumb";
7
7
  import { CreateButton } from "@/components/resources/buttons/create";
8
+ import { useResourceLabel } from "@/components/resources/resource-label";
9
+ import { resolveTranslatableText, type TranslationOptions } from "@/lib/i18n";
8
10
  import { cn } from "@/lib/utils";
9
11
 
10
12
  type ListViewProps = PropsWithChildren<{
@@ -35,22 +37,30 @@ export const ListViewHeader = ({
35
37
  wrapperClassName,
36
38
  headerClassName,
37
39
  }: ListHeaderProps) => {
38
- const getUserFriendlyName = useUserFriendlyName();
39
-
40
40
  const { resource, identifier } = useResourceParams({
41
41
  resource: resourceFromProps,
42
42
  });
43
+ const translate = useTranslate();
43
44
  const resourceName = identifier ?? resource?.name;
44
45
 
45
46
  const isCreateButtonVisible = canCreate ?? !!resource?.create;
46
47
 
47
- const title =
48
- titleFromProps ??
49
- getUserFriendlyName(
50
- resource?.meta?.label ?? identifier ?? resource?.name,
51
- "plural"
52
- );
53
- const description = resource?.meta?.description as string | undefined;
48
+ const resourceTitle = useResourceLabel(resource, "plural", identifier);
49
+ const title = titleFromProps ?? resourceTitle;
50
+ const meta = resource?.meta as
51
+ | {
52
+ description?: string;
53
+ descriptionI18nKey?: string;
54
+ i18nOptions?: TranslationOptions;
55
+ }
56
+ | undefined;
57
+ const description = meta?.descriptionI18nKey
58
+ ? translate(
59
+ meta.descriptionI18nKey,
60
+ meta.i18nOptions,
61
+ meta.description ?? meta.descriptionI18nKey
62
+ )
63
+ : resolveTranslatableText(meta?.description);
54
64
 
55
65
  return (
56
66
  <div className={cn("flex flex-col", "gap-3", wrapperClassName)}>
@@ -3,25 +3,29 @@
3
3
  import type { PropsWithChildren } from "react";
4
4
 
5
5
  import { ArrowLeftIcon } from "lucide-react";
6
- import {
7
- useBack,
8
- useResourceParams,
9
- useUserFriendlyName,
10
- } from "@refinedev/core";
6
+ import { useBack, useResourceParams, useTranslate } from "@refinedev/core";
11
7
  import { Breadcrumb } from "@/components/app-shell/breadcrumb";
12
8
  import { Button } from "@/components/ui/button";
13
9
  import { RefreshButton } from "@/components/resources/buttons/refresh";
14
10
  import { cn } from "@/lib/utils";
15
11
  import { EditButton } from "../buttons/edit";
12
+ import { useResourceLabel } from "@/components/resources/resource-label";
16
13
 
17
14
  type ShowViewProps = PropsWithChildren<{
18
15
  className?: string;
16
+ resource?: string;
17
+ title?: string;
19
18
  }>;
20
19
 
21
- export function ShowView({ children, className }: ShowViewProps) {
20
+ export function ShowView({
21
+ children,
22
+ className,
23
+ resource,
24
+ title,
25
+ }: ShowViewProps) {
22
26
  return (
23
27
  <div className={cn("flex flex-col", "gap-6", className)}>
24
- <ShowViewHeader />
28
+ <ShowViewHeader resource={resource} title={title} />
25
29
  {children}
26
30
  </div>
27
31
  );
@@ -41,8 +45,7 @@ export const ShowViewHeader = ({
41
45
  headerClassName,
42
46
  }: ShowViewHeaderProps) => {
43
47
  const back = useBack();
44
-
45
- const getUserFriendlyName = useUserFriendlyName();
48
+ const translate = useTranslate();
46
49
 
47
50
  const { resource, identifier } = useResourceParams({
48
51
  resource: resourceFromProps,
@@ -51,12 +54,12 @@ export const ShowViewHeader = ({
51
54
 
52
55
  const resourceName = resource?.name ?? identifier;
53
56
 
54
- const title =
55
- titleFromProps ??
56
- getUserFriendlyName(
57
- resource?.meta?.label ?? identifier ?? resource?.name,
58
- "singular"
59
- );
57
+ const resourceTitle = useResourceLabel(resource, "singular", identifier);
58
+ const title = titleFromProps ?? resourceTitle;
59
+ const description = translate(
60
+ "views.show.description",
61
+ "Review the record and its NocoBase-managed data."
62
+ );
60
63
 
61
64
  return (
62
65
  <div className={cn("flex flex-col", "gap-3", wrapperClassName)}>
@@ -75,6 +78,7 @@ export const ShowViewHeader = ({
75
78
  size="icon"
76
79
  className="mt-0.5 rounded-lg"
77
80
  onClick={back}
81
+ aria-label={translate("buttons.cancel", "Cancel")}
78
82
  >
79
83
  <ArrowLeftIcon className="h-4 w-4" />
80
84
  </Button>
@@ -82,9 +86,7 @@ export const ShowViewHeader = ({
82
86
  <h2 className="text-3xl font-semibold tracking-[-0.035em]">
83
87
  {title}
84
88
  </h2>
85
- <p className="mt-2 text-sm text-muted-foreground">
86
- Review the record and its NocoBase-managed data.
87
- </p>
89
+ <p className="mt-2 text-sm text-muted-foreground">{description}</p>
88
90
  </div>
89
91
  </div>
90
92
 
@@ -0,0 +1,19 @@
1
+ # NocoBase ACL UI
2
+
3
+ Reusable ACL composition components for the NocoBase Refine starter.
4
+
5
+ After installation, import the components from `@/extensions/nocobase-acl`.
6
+
7
+ - `AclPage` combines page-level permissions.
8
+ - `AclRegion` hides or replaces individual data regions.
9
+ - `AclField` applies NocoBase field whitelists.
10
+ - `RoleSwitcher` can be placed in any application surface. It supports
11
+ normal roles, Anonymous when enabled, and Full permissions through the
12
+ `__union__` role when the system role mode allows it.
13
+
14
+ Mark non-collection resources with `meta.acl.type = "authenticated"`, or use
15
+ the `collection`, `snippet`, and `route` ACL metadata variants when the resource
16
+ should participate in NocoBase permission checks.
17
+
18
+ The underlying `accessControlProvider`, `roles:check` cache, record permissions,
19
+ and Refine integration are provided by the compatible Starter.
@@ -0,0 +1,106 @@
1
+ import type { BaseKey } from "@refinedev/core";
2
+ import type { PropsWithChildren, ReactNode } from "react";
3
+
4
+ import { AccessDenied } from "@/components/access-control/access-denied";
5
+ import {
6
+ canAccessWithSnapshot,
7
+ useAclSnapshot,
8
+ } from "@/lib/nocobase/acl";
9
+
10
+ export type AclPermission = {
11
+ resource: string;
12
+ action: string;
13
+ id?: BaseKey;
14
+ field?: string;
15
+ dataSourceKey?: string;
16
+ };
17
+
18
+ export type AclPageProps = PropsWithChildren<{
19
+ anyOf?: AclPermission[];
20
+ allOf?: AclPermission[];
21
+ fallback?: ReactNode;
22
+ }>;
23
+
24
+ export type AclRegionProps = PropsWithChildren<
25
+ Omit<AclPermission, "field"> & {
26
+ fallback?: "hidden" | "forbidden" | ReactNode;
27
+ }
28
+ >;
29
+
30
+ export type AclFieldProps = PropsWithChildren<{
31
+ resource: string;
32
+ action: string;
33
+ field: string;
34
+ dataSourceKey?: string;
35
+ fallback?: ReactNode;
36
+ }>;
37
+
38
+ const canAccess = (
39
+ snapshot: ReturnType<typeof useAclSnapshot>,
40
+ permission: AclPermission
41
+ ) =>
42
+ canAccessWithSnapshot(snapshot, {
43
+ resource: permission.resource,
44
+ action: permission.action,
45
+ params: {
46
+ id: permission.id,
47
+ field: permission.field,
48
+ dataSourceKey: permission.dataSourceKey,
49
+ },
50
+ });
51
+
52
+ export function AclPage({
53
+ children,
54
+ anyOf,
55
+ allOf,
56
+ fallback = <AccessDenied />,
57
+ }: AclPageProps) {
58
+ const snapshot = useAclSnapshot();
59
+ const anyAllowed = !anyOf?.length || anyOf.some((item) => canAccess(snapshot, item));
60
+ const allAllowed = !allOf?.length || allOf.every((item) => canAccess(snapshot, item));
61
+
62
+ return anyAllowed && allAllowed ? children : fallback;
63
+ }
64
+
65
+ export function AclRegion({
66
+ children,
67
+ resource,
68
+ action,
69
+ id,
70
+ dataSourceKey,
71
+ fallback = "hidden",
72
+ }: AclRegionProps) {
73
+ const snapshot = useAclSnapshot();
74
+ const allowed = canAccess(snapshot, {
75
+ resource,
76
+ action,
77
+ id,
78
+ dataSourceKey,
79
+ });
80
+
81
+ if (allowed) return children;
82
+ if (fallback === "hidden") return null;
83
+ if (fallback === "forbidden") {
84
+ return <AccessDenied className="min-h-40" />;
85
+ }
86
+ return fallback;
87
+ }
88
+
89
+ export function AclField({
90
+ children,
91
+ resource,
92
+ action,
93
+ field,
94
+ dataSourceKey,
95
+ fallback = null,
96
+ }: AclFieldProps) {
97
+ const snapshot = useAclSnapshot();
98
+ return canAccess(snapshot, {
99
+ resource,
100
+ action,
101
+ field,
102
+ dataSourceKey,
103
+ })
104
+ ? children
105
+ : fallback;
106
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./acl-boundary";
2
+ export * from "./role-options";
3
+ export * from "./role-switcher";
@@ -0,0 +1,36 @@
1
+ import type { Role, RoleMode } from "@/lib/nocobase/acl";
2
+ import { resolveTranslatableText } from "@/lib/i18n";
3
+
4
+ export const UNION_ROLE = "__union__";
5
+ export const ANONYMOUS_ROLE = "anonymous";
6
+
7
+ export function resolveRoleTitle(role?: Role) {
8
+ return resolveTranslatableText(role?.title || role?.name || "Role", {
9
+ ns: "starter",
10
+ });
11
+ }
12
+
13
+ export function getRoleOptions({
14
+ roles,
15
+ roleMode,
16
+ allowAnonymous = false,
17
+ }: {
18
+ roles: Role[];
19
+ roleMode?: RoleMode;
20
+ allowAnonymous?: boolean;
21
+ }) {
22
+ if (roleMode === "only-use-union") {
23
+ return [{ name: UNION_ROLE, title: "Full permissions" }];
24
+ }
25
+
26
+ const options = roles.filter(
27
+ (role) => role.name !== UNION_ROLE && role.name !== ANONYMOUS_ROLE
28
+ );
29
+ if (allowAnonymous) {
30
+ options.push({ name: ANONYMOUS_ROLE, title: "Anonymous" });
31
+ }
32
+ if (roleMode === "allow-use-union") {
33
+ options.unshift({ name: UNION_ROLE, title: "Full permissions" });
34
+ }
35
+ return options;
36
+ }
@@ -0,0 +1,135 @@
1
+ import { useGetIdentity } from "@refinedev/core";
2
+ import { Loader2, ShieldCheck } from "lucide-react";
3
+ import { useMemo, useState, type ReactNode } from "react";
4
+
5
+ import { Badge } from "@/components/ui/badge";
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectSeparator,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from "@/components/ui/select";
14
+ import {
15
+ switchRole,
16
+ useAclSnapshot,
17
+ type AclIdentity,
18
+ type Role,
19
+ } from "@/lib/nocobase/acl";
20
+ import { cn } from "@/lib/utils";
21
+ import { getRoleOptions, resolveRoleTitle, UNION_ROLE } from "./role-options";
22
+
23
+ export type RoleSwitcherProps = {
24
+ className?: string;
25
+ triggerClassName?: string;
26
+ label?: ReactNode | false;
27
+ showWhenUnavailable?: boolean;
28
+ };
29
+
30
+ export function RoleSwitcher({
31
+ className,
32
+ triggerClassName,
33
+ label = "Switch role",
34
+ showWhenUnavailable = false,
35
+ }: RoleSwitcherProps) {
36
+ const { data: identity, isLoading } = useGetIdentity<AclIdentity>();
37
+ const acl = useAclSnapshot();
38
+ const [switching, setSwitching] = useState(false);
39
+ const [error, setError] = useState<string>();
40
+
41
+ const roles = useMemo(
42
+ () =>
43
+ getRoleOptions({
44
+ roles: identity?.roles ?? [],
45
+ roleMode: acl.data.roleMode,
46
+ allowAnonymous: acl.data.allowAnonymous,
47
+ }),
48
+ [acl.data.allowAnonymous, acl.data.roleMode, identity?.roles]
49
+ );
50
+
51
+ const currentRole = acl.data.role ?? roles[0]?.name;
52
+ const canSwitch = roles.length > 1 && acl.data.roleMode !== "only-use-union";
53
+
54
+ if (isLoading) {
55
+ return <Loader2 className="size-4 animate-spin text-muted-foreground" />;
56
+ }
57
+ if (!canSwitch && !showWhenUnavailable) return null;
58
+
59
+ if (!canSwitch) {
60
+ return (
61
+ <div className={cn("flex items-center gap-2 text-sm", className)}>
62
+ <ShieldCheck className="size-4 text-muted-foreground" />
63
+ <span className="text-muted-foreground">Current role</span>
64
+ <Badge variant="secondary">{getRoleTitle(roles, currentRole)}</Badge>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ return (
70
+ <div className={cn("space-y-2", className)}>
71
+ {label === false ? null : (
72
+ <p className="text-xs font-medium text-muted-foreground">{label}</p>
73
+ )}
74
+ <Select
75
+ value={currentRole}
76
+ disabled={switching}
77
+ onValueChange={(value) => {
78
+ if (!value || value === currentRole) return;
79
+ setSwitching(true);
80
+ setError(undefined);
81
+ void switchRole(value, { reloadAcl: false })
82
+ .then(() => window.location.reload())
83
+ .catch((reason) => {
84
+ setError(
85
+ reason instanceof Error
86
+ ? reason.message
87
+ : "Unable to switch role"
88
+ );
89
+ setSwitching(false);
90
+ });
91
+ }}
92
+ >
93
+ <SelectTrigger
94
+ className={cn("w-full min-w-52", triggerClassName)}
95
+ aria-label="Switch role"
96
+ >
97
+ {switching ? <Loader2 className="animate-spin" /> : <ShieldCheck />}
98
+ <SelectValue>{getRoleTitle(roles, currentRole)}</SelectValue>
99
+ </SelectTrigger>
100
+ <SelectContent>
101
+ {roles.map((role, index) => (
102
+ <RoleOption
103
+ key={role.name}
104
+ role={role}
105
+ showSeparator={index === 1 && roles[0]?.name === UNION_ROLE}
106
+ />
107
+ ))}
108
+ </SelectContent>
109
+ </Select>
110
+ {error ? <p className="text-xs text-destructive">{error}</p> : null}
111
+ </div>
112
+ );
113
+ }
114
+
115
+ function RoleOption({
116
+ role,
117
+ showSeparator,
118
+ }: {
119
+ role: Role;
120
+ showSeparator: boolean;
121
+ }) {
122
+ return (
123
+ <>
124
+ {showSeparator ? <SelectSeparator /> : null}
125
+ <SelectItem value={role.name}>{resolveRoleTitle(role)}</SelectItem>
126
+ </>
127
+ );
128
+ }
129
+
130
+ function getRoleTitle(roles: Role[], roleName?: string) {
131
+ return resolveRoleTitle(
132
+ roles.find((role) => role.name === roleName) ??
133
+ (roleName ? { name: roleName } : undefined)
134
+ );
135
+ }
@@ -0,0 +1,186 @@
1
+ import { Card } from "@/components/ui/card";
2
+ import {
3
+ Table,
4
+ TableBody,
5
+ TableCell,
6
+ TableHead,
7
+ TableHeader,
8
+ TableRow,
9
+ } from "@/components/ui/table";
10
+
11
+ type PropRow = [
12
+ name: string,
13
+ type: string,
14
+ defaultValue: string,
15
+ description: string
16
+ ];
17
+
18
+ const pageProps: PropRow[] = [
19
+ [
20
+ "anyOf",
21
+ "AclPermission[]",
22
+ "undefined",
23
+ "Allows the page when any listed permission is available.",
24
+ ],
25
+ [
26
+ "allOf",
27
+ "AclPermission[]",
28
+ "undefined",
29
+ "Requires every listed permission before rendering the page.",
30
+ ],
31
+ [
32
+ "fallback",
33
+ "ReactNode",
34
+ "AccessDenied",
35
+ "Replaces the complete page when its permission test fails.",
36
+ ],
37
+ ["children", "ReactNode", "required", "The protected page content."],
38
+ ];
39
+
40
+ const regionProps: PropRow[] = [
41
+ [
42
+ "resource",
43
+ "string",
44
+ "required",
45
+ "NocoBase collection or ACL resource name.",
46
+ ],
47
+ [
48
+ "action",
49
+ "string",
50
+ "required",
51
+ "Resource action such as list, show, create, edit, or delete.",
52
+ ],
53
+ [
54
+ "id",
55
+ "BaseKey",
56
+ "undefined",
57
+ "Optional record ID for record-scoped permission checks.",
58
+ ],
59
+ [
60
+ "dataSourceKey",
61
+ "string",
62
+ "undefined",
63
+ "Selects a non-main NocoBase data source.",
64
+ ],
65
+ [
66
+ "fallback",
67
+ '"hidden" | "forbidden" | ReactNode',
68
+ '"hidden"',
69
+ "Hides the region or replaces it with a local denied state.",
70
+ ],
71
+ ["children", "ReactNode", "required", "The protected page region."],
72
+ ];
73
+
74
+ const fieldProps: PropRow[] = [
75
+ [
76
+ "resource",
77
+ "string",
78
+ "required",
79
+ "NocoBase collection containing the field.",
80
+ ],
81
+ [
82
+ "action",
83
+ "string",
84
+ "required",
85
+ "The action whose field whitelist should be checked.",
86
+ ],
87
+ ["field", "string", "required", "Exact NocoBase field name."],
88
+ [
89
+ "dataSourceKey",
90
+ "string",
91
+ "undefined",
92
+ "Selects a non-main NocoBase data source.",
93
+ ],
94
+ [
95
+ "fallback",
96
+ "ReactNode",
97
+ "null",
98
+ "Optional read-only replacement for a denied field.",
99
+ ],
100
+ ["children", "ReactNode", "required", "The protected field or field group."],
101
+ ];
102
+
103
+ export function AclBoundaryApi() {
104
+ return (
105
+ <section className="space-y-5 border-t pt-8">
106
+ <div>
107
+ <p className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
108
+ Permission boundaries
109
+ </p>
110
+ <h2 className="mt-2 text-xl font-semibold tracking-tight">
111
+ Components behind the patterns
112
+ </h2>
113
+ <p className="mt-1.5 max-w-3xl text-sm leading-6 text-muted-foreground">
114
+ Use the smallest boundary that matches the intended user experience:
115
+ complete page, independent region, or individual field.
116
+ </p>
117
+ </div>
118
+ <div className="space-y-7">
119
+ <ComponentApi
120
+ title="AclPage"
121
+ description="Protects a complete route or business page. This corresponds to the Page permission pattern above."
122
+ rows={pageProps}
123
+ />
124
+ <ComponentApi
125
+ title="AclRegion"
126
+ description="Protects one independent area inside a larger page. This corresponds to the Region permission pattern."
127
+ rows={regionProps}
128
+ />
129
+ <ComponentApi
130
+ title="AclField"
131
+ description="Protects one field or field group. This corresponds to the Field permission pattern."
132
+ rows={fieldProps}
133
+ />
134
+ </div>
135
+ </section>
136
+ );
137
+ }
138
+
139
+ function ComponentApi({
140
+ title,
141
+ description,
142
+ rows,
143
+ }: {
144
+ title: string;
145
+ description: string;
146
+ rows: PropRow[];
147
+ }) {
148
+ return (
149
+ <div className="space-y-3">
150
+ <div>
151
+ <h3 className="font-semibold">{title}</h3>
152
+ <p className="mt-1 text-sm text-muted-foreground">{description}</p>
153
+ </div>
154
+ <Card className="gap-0 overflow-hidden py-0">
155
+ <Table>
156
+ <TableHeader>
157
+ <TableRow>
158
+ <TableHead>Prop</TableHead>
159
+ <TableHead>Type</TableHead>
160
+ <TableHead>Default</TableHead>
161
+ <TableHead>Description</TableHead>
162
+ </TableRow>
163
+ </TableHeader>
164
+ <TableBody>
165
+ {rows.map(([name, type, defaultValue, propDescription]) => (
166
+ <TableRow key={name}>
167
+ <TableCell className="font-mono text-xs font-medium">
168
+ {name}
169
+ </TableCell>
170
+ <TableCell className="font-mono text-xs text-muted-foreground">
171
+ {type}
172
+ </TableCell>
173
+ <TableCell className="font-mono text-xs text-muted-foreground">
174
+ {defaultValue}
175
+ </TableCell>
176
+ <TableCell className="min-w-72 whitespace-normal text-muted-foreground">
177
+ {propDescription}
178
+ </TableCell>
179
+ </TableRow>
180
+ ))}
181
+ </TableBody>
182
+ </Table>
183
+ </Card>
184
+ </div>
185
+ );
186
+ }