@nocobase/portal-template-default 1.0.16 → 1.0.17

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 (150) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.MD +4 -0
  3. package/dist/assets/{LineDraw-vPRTWiQ5.js → LineDraw-BBB5hwuJ.js} +1 -1
  4. package/dist/assets/{SymbolDraw-DOl_Tss3.js → SymbolDraw-CKS3uUb_.js} +1 -1
  5. package/dist/assets/{VisualMapping-j08W3KBm.js → VisualMapping-BCwaW3TD.js} +1 -1
  6. package/dist/assets/auth-method-demo-BdCCvEEz.js +1 -0
  7. package/dist/assets/{auto-login-provider-Bukw7zEe.js → auto-login-provider-DO5Il6Dp.js} +2 -2
  8. package/dist/assets/auto-login-provider-TQ08B6-i.js +1 -0
  9. package/dist/assets/auto-redirect-provider-ButXp0A3.js +1 -0
  10. package/dist/assets/auto-redirect-provider-DnGGP2Vn.js +1 -0
  11. package/dist/assets/auto-redirect-provider-ja-vFPaF.js +1 -0
  12. package/dist/assets/calendar-C7xnRgOT.js +1 -0
  13. package/dist/assets/{cas-sign-in-button-BQn3Mttx.js → cas-sign-in-button-BZCa4lR6.js} +1 -1
  14. package/dist/assets/{components-BT5mL9gW.js → components-_g-GjQeV.js} +3 -3
  15. package/dist/assets/{configuration-gate-BJoadoA6.js → configuration-gate-B4XVz7tS.js} +1 -1
  16. package/dist/assets/{createClipPathFromCoordSys-m8IbnJ1z.js → createClipPathFromCoordSys-z9Kmh-tu.js} +1 -1
  17. package/dist/assets/createSeriesDataSimply-CPqysF0K.js +1 -0
  18. package/dist/assets/{customGraphicKeyframeAnimation-CYLPurwK.js → customGraphicKeyframeAnimation-DMU5crAu.js} +1 -1
  19. package/dist/assets/demo-BPlae2bp.js +1 -0
  20. package/dist/assets/demo-Bj_Y0RrW.js +1 -0
  21. package/dist/assets/demo-CKPAQOib.js +1 -0
  22. package/dist/assets/demo-CXRTPvIx.js +1 -0
  23. package/dist/assets/demo-DJuVDu1n.js +1 -0
  24. package/dist/assets/demo-DUmWU7ik.js +1 -0
  25. package/dist/assets/demo-NhIzFRE1.js +1 -0
  26. package/dist/assets/{dingtalk-sign-in-button-DSluFaP9.js → dingtalk-sign-in-button-DMPgE5kT.js} +1 -1
  27. package/dist/assets/{echarts-preview-mRXoEciY.js → echarts-preview-R96Pbqmz.js} +1 -1
  28. package/dist/assets/{echarts-runtime-CvLr4Slh.js → echarts-runtime-Ce_z2eX_.js} +3 -3
  29. package/dist/assets/{echarts-runtime-advanced-BC1tvfQ0.js → echarts-runtime-advanced-Dr80KHJR.js} +1 -1
  30. package/dist/assets/{echarts-runtime-common-DhNaV5B8.js → echarts-runtime-common-DB95Yfc1.js} +1 -1
  31. package/dist/assets/{echarts-runtime-components-BiioS8t1.js → echarts-runtime-components-DCJEaTJk.js} +1 -1
  32. package/dist/assets/{echarts-runtime-hierarchy-CAWqYvFh.js → echarts-runtime-hierarchy-CezgZs9O.js} +1 -1
  33. package/dist/assets/{floating-RVbHvIiD.js → floating-O9WmiDXk.js} +2 -2
  34. package/dist/assets/index-BBKV6O5d.css +1 -0
  35. package/dist/assets/{index-Dw20Lzpp.js → index-CPzehPGe.js} +3 -3
  36. package/dist/assets/index-Cc6awvdg.js +1064 -0
  37. package/dist/assets/index-ChA0z9ke.js +38 -0
  38. package/dist/assets/{index-BysibBZu.js → index-DgB7uTMs.js} +2 -2
  39. package/dist/assets/{index-C-tdHirk.js → index-DroUAK80.js} +1 -1
  40. package/dist/assets/index-WKvygTBp.js +59 -0
  41. package/dist/assets/{labelHelper-DbITmOUW.js → labelHelper-Dk5jkiD-.js} +1 -1
  42. package/dist/assets/ldap-sign-in-form-CKN4l7qM.js +1 -0
  43. package/dist/assets/{oidc-sign-in-button-Cgpa8Qzj.js → oidc-sign-in-button-BIRonrpC.js} +1 -1
  44. package/dist/assets/{page-context-AU6RU0Qi.js → page-context-DAiW_dId.js} +4 -4
  45. package/dist/assets/prompt-card-rGZqpqkF.js +1 -0
  46. package/dist/assets/prompt-output-B4h3uLWf.js +1 -0
  47. package/dist/assets/saml-sign-in-button-B4ugaLIb.js +1 -0
  48. package/dist/assets/{sectorHelper-C6L782Ue.js → sectorHelper-BuxrrJrA.js} +1 -1
  49. package/dist/assets/shortcut-BhKXxJLs.js +47 -0
  50. package/dist/assets/{sms-sign-in-form-Bo2WLZuP.js → sms-sign-in-form-DKpxRDdZ.js} +1 -1
  51. package/dist/assets/{static.browser-CoPxsICt.js → static.browser-kzzpH0Xt.js} +1 -1
  52. package/dist/assets/switch-BdVxPIVU.js +1 -0
  53. package/dist/assets/{tool-cards-ipgJ_3Ec.js → tool-cards-CpczIke-.js} +3 -3
  54. package/dist/assets/{use-authenticator-sign-in-BzFeh7qe.js → use-authenticator-sign-in-CIj210ry.js} +1 -1
  55. package/dist/assets/use-dingtalk-sign-in-BW4kkXD3.js +1 -0
  56. package/dist/assets/{use-wecom-sign-in-PPIMLvGa.js → use-wecom-sign-in-dIqMtcN9.js} +1 -1
  57. package/dist/assets/{wecom-sign-in-button-rUq9RW8R.js → wecom-sign-in-button-Dn_2zDab.js} +1 -1
  58. package/dist/index.html +2 -2
  59. package/dist/index.raw.html +2 -2
  60. package/package.json +9 -1
  61. package/src/App.tsx +5 -2
  62. package/src/app/extension.ts +2 -0
  63. package/src/app/extensions.tsx +18 -6
  64. package/src/app/route-access-guard.tsx +22 -0
  65. package/src/app/route-runtime.ts +173 -0
  66. package/src/app/routes.tsx +2 -2
  67. package/src/components/app-shell/header.tsx +56 -1
  68. package/src/components/app-shell/sidebar.tsx +14 -2
  69. package/src/extensions/nocobase-acl/README.md +28 -14
  70. package/src/extensions/nocobase-acl/demo/index.tsx +87 -15
  71. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +1 -0
  72. package/src/extensions/nocobase-mail/README.md +74 -0
  73. package/src/extensions/nocobase-mail/components/index.ts +79 -0
  74. package/src/extensions/nocobase-mail/components/mail-api.ts +423 -0
  75. package/src/extensions/nocobase-mail/components/mail-attachment-list.tsx +67 -0
  76. package/src/extensions/nocobase-mail/components/mail-compose-attachments.tsx +166 -0
  77. package/src/extensions/nocobase-mail/components/mail-compose.tsx +1071 -0
  78. package/src/extensions/nocobase-mail/components/mail-detail.tsx +653 -0
  79. package/src/extensions/nocobase-mail/components/mail-draft-autosave.ts +26 -0
  80. package/src/extensions/nocobase-mail/components/mail-empty.tsx +48 -0
  81. package/src/extensions/nocobase-mail/components/mail-filters.tsx +277 -0
  82. package/src/extensions/nocobase-mail/components/mail-inbox.tsx +544 -0
  83. package/src/extensions/nocobase-mail/components/mail-inline-images.ts +69 -0
  84. package/src/extensions/nocobase-mail/components/mail-label-badge.tsx +25 -0
  85. package/src/extensions/nocobase-mail/components/mail-label-manager.tsx +271 -0
  86. package/src/extensions/nocobase-mail/components/mail-labels-editor.tsx +267 -0
  87. package/src/extensions/nocobase-mail/components/mail-mass-tracking.tsx +271 -0
  88. package/src/extensions/nocobase-mail/components/mail-note-editor.tsx +99 -0
  89. package/src/extensions/nocobase-mail/components/mail-recipient-input.tsx +260 -0
  90. package/src/extensions/nocobase-mail/components/mail-reply-quote.ts +28 -0
  91. package/src/extensions/nocobase-mail/components/mail-rich-editor.tsx +287 -0
  92. package/src/extensions/nocobase-mail/components/mail-send-actions.tsx +238 -0
  93. package/src/extensions/nocobase-mail/components/mail-senders.ts +61 -0
  94. package/src/extensions/nocobase-mail/components/mail-settings-drawer.tsx +708 -0
  95. package/src/extensions/nocobase-mail/components/mail-showcase-layout.tsx +64 -0
  96. package/src/extensions/nocobase-mail/components/mail-signature-manager.tsx +258 -0
  97. package/src/extensions/nocobase-mail/components/mail-signatures.ts +25 -0
  98. package/src/extensions/nocobase-mail/components/mail-table.tsx +476 -0
  99. package/src/extensions/nocobase-mail/components/mail-template-manager.tsx +204 -0
  100. package/src/extensions/nocobase-mail/components/mail-toolbar.tsx +155 -0
  101. package/src/extensions/nocobase-mail/components/mail-unread.tsx +111 -0
  102. package/src/extensions/nocobase-mail/components/types.ts +297 -0
  103. package/src/extensions/nocobase-mail/components/use-mail-compose.tsx +262 -0
  104. package/src/extensions/nocobase-mail/components/use-mail-mass-messages.ts +54 -0
  105. package/src/extensions/nocobase-mail/components/use-mail-messages.ts +113 -0
  106. package/src/extensions/nocobase-mail/components/use-mail-signatures.ts +122 -0
  107. package/src/extensions/nocobase-mail/components/use-mail-templates.ts +54 -0
  108. package/src/extensions/nocobase-mail/extension.tsx +105 -0
  109. package/src/extensions/nocobase-mail/index.ts +1 -0
  110. package/src/extensions/nocobase-mail/mail-demo-pages.tsx +503 -0
  111. package/src/extensions/nocobase-mail/mail-pages.tsx +201 -0
  112. package/src/extensions/nocobase-mail/tests/mail-quote-html-regression.mjs +51 -0
  113. package/src/extensions/nocobase-mail/tests/mail-regression.mjs +649 -0
  114. package/src/extensions/nocobase-users-example/app-routes.tsx +121 -0
  115. package/src/extensions/nocobase-users-example/extension.tsx +2 -100
  116. package/src/extensions/nocobase-users-example/index.ts +1 -0
  117. package/src/lib/nocobase/acl/menu.ts +17 -3
  118. package/src/lib/nocobase/acl/types.ts +4 -0
  119. package/src/locales/en-US.ts +2 -1
  120. package/src/locales/zh-CN.ts +2 -1
  121. package/src/providers/runtime-config.ts +18 -0
  122. package/src/routes.tsx +10 -0
  123. package/src/vite-env.d.ts +3 -0
  124. package/vite.config.ts +8 -0
  125. package/dist/assets/auth-method-demo-B2qcpY8z.js +0 -1
  126. package/dist/assets/auto-login-provider-BMtzvdxG.js +0 -1
  127. package/dist/assets/auto-redirect-provider-C6GNPg5r.js +0 -1
  128. package/dist/assets/auto-redirect-provider-D5c6kN2R.js +0 -1
  129. package/dist/assets/auto-redirect-provider-iTzGEDpU.js +0 -1
  130. package/dist/assets/calendar-Dv2RNnsr.js +0 -1
  131. package/dist/assets/card-C9Hpeq1c.js +0 -1
  132. package/dist/assets/createSeriesDataSimply-DKUV5b5e.js +0 -1
  133. package/dist/assets/demo-BWGOwqtJ.js +0 -1
  134. package/dist/assets/demo-BWbcvkya.js +0 -1
  135. package/dist/assets/demo-DIASSPgn.js +0 -1
  136. package/dist/assets/demo-DuZ6asUU.js +0 -1
  137. package/dist/assets/demo-Dxcy4bDE.js +0 -1
  138. package/dist/assets/demo-YpEC76uw.js +0 -1
  139. package/dist/assets/demo-wzkoR8T2.js +0 -1
  140. package/dist/assets/index-CBZbj45X.js +0 -58
  141. package/dist/assets/index-CgkdZ6Oc.js +0 -686
  142. package/dist/assets/index-NH3aqqQz.css +0 -1
  143. package/dist/assets/index-hSBzW-VA.js +0 -43
  144. package/dist/assets/ldap-sign-in-form-BOMNDTkK.js +0 -1
  145. package/dist/assets/prompt-card-FWugRgi-.js +0 -1
  146. package/dist/assets/prompt-output-DMTuTNab.js +0 -1
  147. package/dist/assets/saml-sign-in-button-qt8bMeoQ.js +0 -1
  148. package/dist/assets/shortcut-Bu7jT_-w.js +0 -47
  149. package/dist/assets/switch-DQkkIE7G.js +0 -1
  150. package/dist/assets/use-dingtalk-sign-in-mrq6K5Yx.js +0 -1
@@ -1 +1 @@
1
- import{b5 as i}from"./index-CgkdZ6Oc.js";function o(r){const n=i();return{signIn:t=>n.mutateAsync({...t,authenticator:r}),isPending:n.isPending,error:n.error}}export{o as u};
1
+ import{bf as i}from"./index-Cc6awvdg.js";function o(r){const n=i();return{signIn:t=>n.mutateAsync({...t,authenticator:r}),isPending:n.isPending,error:n.error}}export{o as u};
@@ -0,0 +1 @@
1
+ import{b5 as g,b as a,b6 as d,b7 as i}from"./index-Cc6awvdg.js";function w(){return typeof navigator<"u"&&navigator.userAgent.toLowerCase().includes("dingtalk")}function b(n){const[r]=g(),[c,o]=a.useState(!1),[l,s]=a.useState(),u=a.useMemo(()=>{if(r.get("authenticator")!==n.name)return;const e=r.get("error");return e?new Error(e):void 0},[n.name,r]);return{signIn:a.useCallback(async()=>{s(void 0),o(!0);try{const e=d(r.get("to")||r.get("redirect")||"/"),t=await i.action("dingtalk","getAuthUrl",{method:"POST",authenticator:n.name,includeRole:!1,withAclMeta:!1,body:{redirect:e}});if(!(t!=null&&t.url))throw new Error("NocoBase did not return a DingTalk authorization URL.");window.location.replace(i.resolveUrl(t.url))}catch(e){s(e instanceof Error?e:new Error("Unable to start DingTalk sign-in.")),o(!1)}},[n.name,r]),isPending:c,error:l??u}}export{w as i,b as u};
@@ -1 +1 @@
1
- import{aX as m,b as n,aY as d,aZ as i}from"./index-CgkdZ6Oc.js";function g(){return typeof navigator<"u"&&navigator.userAgent.toLowerCase().includes("wxwork")}function h(o){const[r]=m(),[c,a]=n.useState(!1),[l,s]=n.useState(),u=n.useMemo(()=>{if(r.get("authenticator")!==o.name)return;const e=r.get("errorMsg");return e?new Error(e):void 0},[o.name,r]);return{signIn:n.useCallback(async()=>{s(void 0),a(!0);try{const e=d(r.get("to")||r.get("redirect")||"/"),t=await i.action("wecom","getAuthUrl",{method:"POST",authenticator:o.name,includeRole:!1,withAclMeta:!1,body:{redirect:e,isWeComBrowser:g()}});if(!(t!=null&&t.url))throw new Error("NocoBase did not return a WeCom authorization URL.");window.location.replace(i.resolveUrl(t.url))}catch(e){s(e instanceof Error?e:new Error("Unable to start WeCom sign-in.")),a(!1)}},[o.name,r]),isPending:c,error:l??u}}export{g as i,h as u};
1
+ import{b5 as m,b as n,b6 as d,b7 as i}from"./index-Cc6awvdg.js";function g(){return typeof navigator<"u"&&navigator.userAgent.toLowerCase().includes("wxwork")}function b(o){const[r]=m(),[c,s]=n.useState(!1),[l,a]=n.useState(),u=n.useMemo(()=>{if(r.get("authenticator")!==o.name)return;const e=r.get("errorMsg");return e?new Error(e):void 0},[o.name,r]);return{signIn:n.useCallback(async()=>{a(void 0),s(!0);try{const e=d(r.get("to")||r.get("redirect")||"/"),t=await i.action("wecom","getAuthUrl",{method:"POST",authenticator:o.name,includeRole:!1,withAclMeta:!1,body:{redirect:e,isWeComBrowser:g()}});if(!(t!=null&&t.url))throw new Error("NocoBase did not return a WeCom authorization URL.");window.location.replace(i.resolveUrl(t.url))}catch(e){a(e instanceof Error?e:new Error("Unable to start WeCom sign-in.")),s(!1)}},[o.name,r]),isPending:c,error:l??u}}export{g as i,b as u};
@@ -1 +1 @@
1
- import{j as s,D as c,b6 as d,r as p,aD as x,aF as m,aG as g,b7 as u,b8 as j,b9 as T}from"./index-CgkdZ6Oc.js";import{u as b}from"./use-wecom-sign-in-PPIMLvGa.js";function f(e){return typeof e=="string"?e.replace(/<[^>]+>/g," ").replace(/\s+/g," ").trim():""}function y({authenticator:e,onSignIn:i}){var o;const{signIn:a,isPending:t,error:n}=b(e),r=f((o=e.options)==null?void 0:o.btnTooltip),l=s.jsxs(c,{type:"button",variant:"outline",className:"w-full",disabled:!i&&t,onClick:i??a,children:[s.jsx(d,{}),!i&&t?"Redirecting…":p(e.title||e.authTypeTitle||"Sign in via WeCom")]});return s.jsxs("div",{className:"space-y-3",children:[n&&s.jsxs(x,{variant:"destructive",children:[s.jsx(m,{children:"WeCom sign-in failed"}),s.jsx(g,{children:n.message})]}),r?s.jsxs(u,{children:[s.jsx(j,{render:l}),s.jsx(T,{side:"right",className:"max-w-xs",children:r})]}):l]})}export{y as default};
1
+ import{j as s,K as c,bg as d,r as p,aN as x,aP as m,aQ as g,bh as u,bi as j,bj as T}from"./index-Cc6awvdg.js";import{u as b}from"./use-wecom-sign-in-dIqMtcN9.js";function f(e){return typeof e=="string"?e.replace(/<[^>]+>/g," ").replace(/\s+/g," ").trim():""}function y({authenticator:e,onSignIn:i}){var o;const{signIn:a,isPending:t,error:n}=b(e),r=f((o=e.options)==null?void 0:o.btnTooltip),l=s.jsxs(c,{type:"button",variant:"outline",className:"w-full",disabled:!i&&t,onClick:i??a,children:[s.jsx(d,{}),!i&&t?"Redirecting…":p(e.title||e.authTypeTitle||"Sign in via WeCom")]});return s.jsxs("div",{className:"space-y-3",children:[n&&s.jsxs(x,{variant:"destructive",children:[s.jsx(m,{children:"WeCom sign-in failed"}),s.jsx(g,{children:n.message})]}),r?s.jsxs(u,{children:[s.jsx(j,{render:l}),s.jsx(T,{side:"right",className:"max-w-xs",children:r})]}):l]})}export{y as default};
package/dist/index.html CHANGED
@@ -20,8 +20,8 @@
20
20
  content="/logo-mark.png"
21
21
  />
22
22
  <title>NocoBase</title>
23
- <script type="module" crossorigin src="/assets/index-CgkdZ6Oc.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-NH3aqqQz.css">
23
+ <script type="module" crossorigin src="/assets/index-Cc6awvdg.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-BBKV6O5d.css">
25
25
  </head>
26
26
  <body>
27
27
  <noscript>You need to enable JavaScript to run this app.</noscript>
@@ -29,8 +29,8 @@
29
29
  window.__nocobase_api_client_share_token__ = window.__nocobase_api_client_share_token__ ?? false;
30
30
  </script>
31
31
  <!-- nocobase-runtime-config:end -->
32
- <script type="module" crossorigin src="/assets/index-CgkdZ6Oc.js"></script>
33
- <link rel="stylesheet" crossorigin href="/assets/index-NH3aqqQz.css">
32
+ <script type="module" crossorigin src="/assets/index-Cc6awvdg.js"></script>
33
+ <link rel="stylesheet" crossorigin href="/assets/index-BBKV6O5d.css">
34
34
  </head>
35
35
  <body>
36
36
  <noscript>You need to enable JavaScript to run this app.</noscript>
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@nocobase/portal-template-default",
3
- "version": "1.0.16",
3
+ "displayName": "Default Template",
4
+ "version": "1.0.17",
4
5
  "packageManager": "pnpm@10.28.0",
5
6
  "type": "module",
6
7
  "devDependencies": {
@@ -24,6 +25,12 @@
24
25
  "@tailwindcss/vite": "^4.1.8",
25
26
  "@tanstack/react-query": "^5.81.5",
26
27
  "@tanstack/react-table": "^8.2.6",
28
+ "@tiptap/extension-link": "^3.29.0",
29
+ "@tiptap/extension-placeholder": "^3.29.0",
30
+ "@tiptap/extension-text-style": "3.29.0",
31
+ "@tiptap/pm": "^3.29.0",
32
+ "@tiptap/react": "^3.29.0",
33
+ "@tiptap/starter-kit": "^3.29.0",
27
34
  "@types/node": "^20",
28
35
  "@types/react": "^19.1.0",
29
36
  "@types/react-dom": "^19.1.0",
@@ -84,6 +91,7 @@
84
91
  "test:auth": "node tests/auth-session-regression.mjs",
85
92
  "test:acl": "node registry/nocobase-acl/tests/acl-regression.mjs",
86
93
  "test:i18n": "node registry/nocobase-i18n/tests/i18n-regression.mjs",
94
+ "test:mail": "node registry/nocobase-mail/tests/mail-regression.mjs",
87
95
  "start": "refine start",
88
96
  "refine": "refine"
89
97
  },
package/src/App.tsx CHANGED
@@ -11,7 +11,10 @@ import { Toaster } from "./components/notifications/toaster";
11
11
  import { ThemeProvider } from "./components/theme/theme-provider";
12
12
  import { TooltipProvider } from "./components/ui/tooltip";
13
13
  import { BrandLogo } from "./components/app-shell/brand";
14
- import { AppAuthRuntimeProviders, extensionResources } from "./app/extensions";
14
+ import {
15
+ AppAuthRuntimeProviders,
16
+ configuredResources,
17
+ } from "./app/extensions";
15
18
  import "./App.css";
16
19
  import { authProvider } from "./providers/auth";
17
20
  import { accessControlProvider } from "./providers/access-control";
@@ -24,7 +27,7 @@ import { AppRoutes } from "./app/routes";
24
27
  const getResourcePriority = (resource: ResourceProps) =>
25
28
  typeof resource.meta?.priority === "number" ? resource.meta.priority : 100;
26
29
 
27
- const appResources = [...extensionResources].sort(
30
+ const appResources = [...configuredResources].sort(
28
31
  (left, right) => getResourcePriority(left) - getResourcePriority(right)
29
32
  );
30
33
 
@@ -1,12 +1,14 @@
1
1
  import type { ResourceProps } from "@refinedev/core";
2
2
  import type { ComponentType, PropsWithChildren, ReactElement } from "react";
3
3
  import type { AuthenticatorAdapter } from "@/components/auth/types";
4
+ import type { AppRouteDefinition } from "./route-runtime";
4
5
 
5
6
  export type AppExtension = {
6
7
  id: string;
7
8
  priority?: number;
8
9
  resources?: ResourceProps[];
9
10
  routes?: ReactElement;
11
+ appRoutes?: AppRouteDefinition[];
10
12
  dev?: {
11
13
  resources?: ResourceProps[];
12
14
  routes?: ReactElement;
@@ -6,7 +6,9 @@ import {
6
6
  } from "react";
7
7
  import type { AppExtension } from "./extension";
8
8
  import { LoadingState } from "@/components/app-shell/loading-state";
9
+ import { appRoutes, registryRoutesEnabled } from "@/routes";
9
10
  import { createDevelopmentRoute } from "./development";
11
+ import { buildRouteResources, renderAppRoutes } from "./route-runtime";
10
12
 
11
13
  const extensionModules = import.meta.glob<{ default: AppExtension }>(
12
14
  "@/extensions/*/extension.tsx",
@@ -21,13 +23,23 @@ export const appExtensions = Object.values(extensionModules)
21
23
  left.id.localeCompare(right.id)
22
24
  );
23
25
 
24
- export const extensionResources = appExtensions.flatMap(
25
- (extension) => extension.resources ?? []
26
- );
26
+ const routeExtensions = registryRoutesEnabled ? appExtensions : [];
27
+ const routeDefinitions = [
28
+ ...appRoutes,
29
+ ...routeExtensions.flatMap((extension) => extension.appRoutes ?? []),
30
+ ];
31
+
32
+ export const configuredResources = [
33
+ ...buildRouteResources(routeDefinitions),
34
+ ...routeExtensions.flatMap((extension) => extension.resources ?? []),
35
+ ];
27
36
 
28
- export const extensionRouteElements = appExtensions
29
- .map((extension) => extension.routes)
30
- .filter((routes): routes is ReactElement => Boolean(routes));
37
+ export const configuredRouteElements = [
38
+ ...renderAppRoutes(routeDefinitions),
39
+ ...routeExtensions
40
+ .map((extension) => extension.routes)
41
+ .filter((routes): routes is ReactElement => Boolean(routes)),
42
+ ];
31
43
 
32
44
  export const extensionStandaloneRouteElements = import.meta.env.DEV
33
45
  ? [createDevelopmentRoute(appExtensions)]
@@ -0,0 +1,22 @@
1
+ import type { PropsWithChildren, ReactNode } from "react";
2
+
3
+ import { AccessDenied } from "@/components/access-control/access-denied";
4
+ import { CanAccess } from "@/components/access-control/can-access";
5
+ import type { RouteAccessConstraint } from "@/lib/nocobase/acl";
6
+
7
+ export function RouteAccessGuard({
8
+ access,
9
+ children,
10
+ fallback = <AccessDenied />,
11
+ }: PropsWithChildren<{
12
+ access?: RouteAccessConstraint;
13
+ fallback?: ReactNode;
14
+ }>) {
15
+ if (!access) return children;
16
+
17
+ return (
18
+ <CanAccess roles={access.roles} fallback={fallback}>
19
+ {children}
20
+ </CanAccess>
21
+ );
22
+ }
@@ -0,0 +1,173 @@
1
+ import type { ResourceProps } from "@refinedev/core";
2
+ import { createElement, type ReactElement, type ReactNode } from "react";
3
+ import { Outlet, Route } from "react-router";
4
+
5
+ import type { ResourceAcl, RouteAccessConstraint } from "@/lib/nocobase/acl";
6
+ import { RouteAccessGuard } from "./route-access-guard";
7
+
8
+ export { RouteAccessGuard } from "./route-access-guard";
9
+
10
+ type ResourceRouteAction = "create" | "edit" | "show";
11
+
12
+ export type AppRouteResource = Omit<
13
+ ResourceProps,
14
+ "name" | "list" | ResourceRouteAction
15
+ >;
16
+
17
+ type AppRouteBase = {
18
+ name: string;
19
+ element?: ReactNode;
20
+ access?: RouteAccessConstraint;
21
+ };
22
+
23
+ type AppRouteResourceBinding =
24
+ | {
25
+ resource: AppRouteResource;
26
+ resourceAction?: never;
27
+ }
28
+ | {
29
+ resource?: never;
30
+ resourceAction: ResourceRouteAction;
31
+ }
32
+ | {
33
+ resource?: never;
34
+ resourceAction?: never;
35
+ };
36
+
37
+ type AppIndexRouteDefinition = AppRouteBase &
38
+ AppRouteResourceBinding & {
39
+ index: true;
40
+ path?: never;
41
+ children?: never;
42
+ };
43
+
44
+ type AppPathRouteDefinition = AppRouteBase &
45
+ AppRouteResourceBinding & {
46
+ index?: false;
47
+ path?: string;
48
+ children?: AppRouteDefinition[];
49
+ };
50
+
51
+ export type AppRouteDefinition =
52
+ | AppIndexRouteDefinition
53
+ | AppPathRouteDefinition;
54
+
55
+ export function defineAppRoutes<const Routes extends AppRouteDefinition[]>(
56
+ routes: Routes
57
+ ) {
58
+ return routes;
59
+ }
60
+
61
+ const joinRoutePath = (parentPath: string, path?: string) => {
62
+ if (!path) return parentPath || "/";
63
+ if (path.startsWith("/")) return path;
64
+ return `${parentPath.replace(/\/$/, "")}/${path}`.replace(/\/+/g, "/");
65
+ };
66
+
67
+ const getResourceActionRoutes = (
68
+ routes: AppRouteDefinition[],
69
+ parentPath: string,
70
+ resourceName: string,
71
+ actions: Partial<Record<ResourceRouteAction, string>> = {}
72
+ ): Partial<Record<ResourceRouteAction, string>> => {
73
+ for (const route of routes) {
74
+ if (route.resource) continue;
75
+ const fullPath = joinRoutePath(parentPath, route.path);
76
+ if (route.resourceAction) {
77
+ if (actions[route.resourceAction] !== undefined) {
78
+ throw new Error(
79
+ `Resource "${resourceName}" declares multiple ${route.resourceAction} routes.`
80
+ );
81
+ }
82
+ actions[route.resourceAction] = fullPath;
83
+ }
84
+ getResourceActionRoutes(
85
+ route.children ?? [],
86
+ fullPath,
87
+ resourceName,
88
+ actions
89
+ );
90
+ }
91
+
92
+ return actions;
93
+ };
94
+
95
+ const getDefaultResourceAcl = (meta?: ResourceProps["meta"]): ResourceAcl =>
96
+ typeof meta?.acl === "undefined"
97
+ ? { type: "authenticated" }
98
+ : (meta.acl as ResourceAcl);
99
+
100
+ export function buildRouteResources(
101
+ routes: AppRouteDefinition[],
102
+ parentPath = "",
103
+ parentResource?: string,
104
+ inheritedAccess: RouteAccessConstraint[] = []
105
+ ): ResourceProps[] {
106
+ return routes.flatMap((route) => {
107
+ if (route.resourceAction && !parentResource) {
108
+ throw new Error(
109
+ `Route "${route.name}" declares resourceAction without a parent resource.`
110
+ );
111
+ }
112
+
113
+ const fullPath = joinRoutePath(parentPath, route.path);
114
+ const routeAccess = route.access
115
+ ? [...inheritedAccess, route.access]
116
+ : inheritedAccess;
117
+ const resource = route.resource
118
+ ? {
119
+ name: route.name,
120
+ list: route.index
121
+ ? parentPath || "/"
122
+ : route.path
123
+ ? fullPath
124
+ : undefined,
125
+ ...getResourceActionRoutes(
126
+ route.children ?? [],
127
+ fullPath,
128
+ route.name
129
+ ),
130
+ ...route.resource,
131
+ meta: {
132
+ ...route.resource.meta,
133
+ parent: route.resource.meta?.parent ?? parentResource,
134
+ acl: getDefaultResourceAcl(route.resource.meta),
135
+ routeAccess: routeAccess.length ? routeAccess : undefined,
136
+ },
137
+ }
138
+ : undefined;
139
+ const childResources = buildRouteResources(
140
+ route.children ?? [],
141
+ fullPath,
142
+ resource ? route.name : parentResource,
143
+ routeAccess
144
+ );
145
+
146
+ return resource ? [resource, ...childResources] : childResources;
147
+ });
148
+ }
149
+
150
+ export function renderAppRoutes(routes: AppRouteDefinition[]): ReactElement[] {
151
+ return routes.map((route) => {
152
+ const content = route.element ?? createElement(Outlet);
153
+ const element = route.access
154
+ ? createElement(RouteAccessGuard, { access: route.access }, content)
155
+ : content;
156
+
157
+ return route.index
158
+ ? createElement(Route, {
159
+ key: route.name,
160
+ index: true,
161
+ element,
162
+ })
163
+ : createElement(
164
+ Route,
165
+ {
166
+ key: route.name,
167
+ path: route.path,
168
+ element,
169
+ },
170
+ renderAppRoutes(route.children ?? [])
171
+ );
172
+ });
173
+ }
@@ -11,7 +11,7 @@ import { Login } from "@/pages/login";
11
11
  import { Register } from "@/pages/register";
12
12
  import {
13
13
  AppExtensionProviders,
14
- extensionRouteElements,
14
+ configuredRouteElements,
15
15
  extensionStandaloneRouteElements,
16
16
  } from "./extensions";
17
17
 
@@ -41,7 +41,7 @@ export function AppRoutes() {
41
41
  }
42
42
  >
43
43
  <Route index element={<NavigateToAccessibleResource />} />
44
- {extensionRouteElements}
44
+ {configuredRouteElements}
45
45
  <Route path="*" element={<ErrorComponent />} />
46
46
  </Route>
47
47
  </Route>
@@ -11,13 +11,31 @@ import {
11
11
  } from "@/components/ui/dropdown-menu";
12
12
  import { DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
13
13
  import { ThemeToggle } from "@/components/theme/theme-toggle";
14
+ import { Button } from "@/components/ui/button";
15
+ import {
16
+ Tooltip,
17
+ TooltipContent,
18
+ TooltipTrigger,
19
+ } from "@/components/ui/tooltip";
14
20
  import { UserAvatar } from "@/components/app-shell/user-avatar";
15
21
  import { UserInfo } from "@/components/app-shell/user-info";
22
+ import { CanAccess } from "@/components/access-control/can-access";
16
23
  import { useSidebar, SidebarTrigger } from "@/components/ui/sidebar";
17
- import { LogOutIcon } from "lucide-react";
24
+ import { LogOutIcon, SettingsIcon } from "lucide-react";
18
25
  import { cn } from "@/lib/utils";
19
26
  import { Brand } from "@/components/app-shell/brand";
20
27
  import { extensionUserMenuItems } from "@/app/extensions";
28
+ import { resolveNocoBaseServerUrl } from "@/providers/runtime-config";
29
+
30
+ const pluginSettingsResource = {
31
+ name: "plugin-settings",
32
+ meta: {
33
+ acl: {
34
+ type: "snippet",
35
+ name: "pm.*",
36
+ },
37
+ },
38
+ } as const;
21
39
 
22
40
  export const Header = () => {
23
41
  const { isMobile } = useSidebar();
@@ -55,6 +73,7 @@ function DesktopHeader() {
55
73
  </span>
56
74
  </div>
57
75
  <div className="flex items-center gap-2">
76
+ <SettingsLink />
58
77
  <ThemeToggle />
59
78
  <UserDropdown />
60
79
  </div>
@@ -92,6 +111,7 @@ function MobileHeader() {
92
111
  />
93
112
  <Brand logoClassName="h-6" />
94
113
  <div className="flex shrink-0 items-center gap-1">
114
+ <SettingsLink className="size-9" />
95
115
  <ThemeToggle className="size-9" />
96
116
  <UserDropdown />
97
117
  </div>
@@ -99,6 +119,41 @@ function MobileHeader() {
99
119
  );
100
120
  }
101
121
 
122
+ function SettingsLink({ className }: { className?: string }) {
123
+ const translate = useTranslate();
124
+ const label = translate("shell.settings", "System settings");
125
+
126
+ return (
127
+ <CanAccess resourceItem={pluginSettingsResource}>
128
+ <Tooltip>
129
+ <TooltipTrigger
130
+ render={
131
+ <Button
132
+ render={
133
+ <a
134
+ href={resolveNocoBaseServerUrl("/settings")}
135
+ target="_blank"
136
+ rel="noopener noreferrer"
137
+ />
138
+ }
139
+ variant="outline"
140
+ size="icon"
141
+ className={cn(
142
+ "size-10 rounded-xl border-border/70 bg-background/60",
143
+ className
144
+ )}
145
+ >
146
+ <SettingsIcon />
147
+ <span className="sr-only">{label}</span>
148
+ </Button>
149
+ }
150
+ />
151
+ <TooltipContent>{label}</TooltipContent>
152
+ </Tooltip>
153
+ </CanAccess>
154
+ );
155
+ }
156
+
102
157
  const UserDropdown = () => {
103
158
  const { mutate: logout, isPending: isLoggingOut } = useLogout();
104
159
  const translate = useTranslate();
@@ -294,6 +294,7 @@ function SidebarFooter() {
294
294
  return (
295
295
  <ShadcnSidebarFooter className="border-t border-sidebar-border/70 p-0">
296
296
  <div
297
+ title={`${__PORTAL_TEMPLATE_NAME__} v${__PORTAL_TEMPLATE_VERSION__}`}
297
298
  className={cn(
298
299
  "flex min-h-16 items-center",
299
300
  open ? "gap-3 px-5 py-3" : "justify-center px-2"
@@ -306,11 +307,22 @@ function SidebarFooter() {
306
307
  {translate("shell.footer.freedom", "AI builds freely.")}
307
308
  </div>
308
309
  <div className="text-muted-foreground">
310
+ <a
311
+ href="https://nocobase.com"
312
+ target="_blank"
313
+ rel="noopener noreferrer"
314
+ className="font-medium text-sidebar-foreground hover:underline"
315
+ >
316
+ NocoBase
317
+ </a>{" "}
309
318
  {translate(
310
- "shell.footer.reliability",
311
- "NocoBase keeps it reliable."
319
+ "shell.footer.reliabilitySuffix",
320
+ "keeps it reliable."
312
321
  )}
313
322
  </div>
323
+ <div className="mt-1 font-mono text-[10px] text-muted-foreground/70">
324
+ {__PORTAL_TEMPLATE_NAME__} v{__PORTAL_TEMPLATE_VERSION__}
325
+ </div>
314
326
  </div>
315
327
  )}
316
328
  </div>
@@ -12,11 +12,17 @@ After installation, import the components from `@/extensions/nocobase-acl`.
12
12
  normal roles, Anonymous when enabled, and Full permissions through the
13
13
  `__union__` role when the system role mode allows it.
14
14
 
15
- Route resource metadata and `AclPage` accept a `roles` constraint with `anyOf`,
16
- `allOf`, and `noneOf`. Role constraints use the roles in the current effective
17
- ACL context. In union mode this is the set of roles participating in the union;
18
- in single-role mode it is only the selected role. Regions, fields, and actions
19
- continue to use resource permissions.
15
+ Application route definitions and `AclPage` accept a `roles` constraint with
16
+ `anyOf`, `allOf`, and `noneOf`. Put route-level constraints in `access.roles`
17
+ through `defineAppRoutes`; the Starter applies the same constraint to menu
18
+ visibility and direct URL access. Role constraints use the roles in the current
19
+ effective ACL context. In union mode this is the set of roles participating in
20
+ the union; in single-role mode it is only the selected role. Regions, fields,
21
+ and actions continue to use resource permissions.
22
+
23
+ Nested routes inherit their parents' route access. The generated navigation
24
+ item evaluates every constraint in that route chain, matching the nested route
25
+ guards used for direct URL access.
20
26
 
21
27
  Use the Starter's `useGetRoles()` hook when application UI needs the same
22
28
  effective role names. Its result follows the familiar Refine query shape with
@@ -35,15 +41,23 @@ function CurrentAccessContext() {
35
41
  ```
36
42
 
37
43
  ```tsx
38
- const adminRoute = {
39
- name: "administration",
40
- meta: {
41
- acl: {
42
- type: "authenticated",
44
+ import { defineAppRoutes } from "@/app/route-runtime";
45
+
46
+ export const appRoutes = defineAppRoutes([
47
+ {
48
+ name: "administration",
49
+ path: "/administration",
50
+ element: <AdministrationPage />,
51
+ resource: {
52
+ meta: {
53
+ label: "Administration",
54
+ },
55
+ },
56
+ access: {
43
57
  roles: { anyOf: ["admin"] },
44
58
  },
45
59
  },
46
- };
60
+ ]);
47
61
 
48
62
  function AuditLogsRoute() {
49
63
  return (
@@ -56,9 +70,9 @@ function AuditLogsRoute() {
56
70
  }
57
71
  ```
58
72
 
59
- Mark non-collection resources with `meta.acl.type = "authenticated"`, or use
60
- the `collection`, `snippet`, and `route` ACL metadata variants when the resource
61
- should participate in NocoBase permission checks.
73
+ The route runtime marks generated non-collection resources as authenticated.
74
+ Use the `collection`, `snippet`, and `route` ACL metadata variants when a
75
+ resource should also participate in NocoBase permission checks.
62
76
 
63
77
  The underlying `accessControlProvider`, `roles:check` cache, record permissions,
64
78
  and Refine integration are provided by the compatible Starter.