@nocobase/portal-template-default 1.0.4 → 1.0.6

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 (139) hide show
  1. package/README.MD +77 -0
  2. package/package.json +23 -17
  3. package/registry/nocobase-acl/demo/prompt-generator.tsx +6 -39
  4. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +6 -42
  5. package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +1 -1
  6. package/registry/nocobase-ai/demo/prompt-card.tsx +7 -28
  7. package/registry/nocobase-ai/demo/prompt-generator.tsx +9 -30
  8. package/registry/nocobase-ai/demo/shortcut.tsx +1 -1
  9. package/registry/nocobase-ai/demo/tool-cards.tsx +10 -30
  10. package/registry/nocobase-auth-cas/README.md +6 -0
  11. package/registry/nocobase-auth-cas/cas-sign-in-button.tsx +41 -0
  12. package/registry/nocobase-auth-cas/demo.tsx +20 -0
  13. package/registry/nocobase-auth-cas/extension.tsx +46 -0
  14. package/registry/nocobase-auth-cas/index.ts +1 -0
  15. package/registry/nocobase-auth-cas/use-cas-sign-in.ts +29 -0
  16. package/registry/nocobase-auth-dingtalk/README.md +7 -0
  17. package/registry/nocobase-auth-dingtalk/auto-login-provider.tsx +154 -0
  18. package/registry/nocobase-auth-dingtalk/demo.tsx +23 -0
  19. package/registry/nocobase-auth-dingtalk/dingtalk-sign-in-button.tsx +45 -0
  20. package/registry/nocobase-auth-dingtalk/extension.tsx +55 -0
  21. package/registry/nocobase-auth-dingtalk/index.ts +4 -0
  22. package/registry/nocobase-auth-dingtalk/use-dingtalk-sign-in.ts +64 -0
  23. package/registry/nocobase-auth-ldap/README.md +6 -0
  24. package/registry/nocobase-auth-ldap/demo.tsx +21 -0
  25. package/registry/nocobase-auth-ldap/extension.tsx +46 -0
  26. package/registry/nocobase-auth-ldap/index.ts +1 -0
  27. package/registry/nocobase-auth-ldap/ldap-sign-in-form.tsx +69 -0
  28. package/registry/nocobase-auth-ldap/use-ldap-sign-in.ts +5 -0
  29. package/registry/nocobase-auth-oidc/README.md +11 -0
  30. package/registry/nocobase-auth-oidc/auto-redirect-provider.tsx +13 -0
  31. package/registry/nocobase-auth-oidc/demo.tsx +20 -0
  32. package/registry/nocobase-auth-oidc/extension.tsx +51 -0
  33. package/registry/nocobase-auth-oidc/index.ts +1 -0
  34. package/registry/nocobase-auth-oidc/oidc-sign-in-button.tsx +44 -0
  35. package/registry/nocobase-auth-oidc/use-oidc-sign-in.ts +58 -0
  36. package/registry/nocobase-auth-saml/README.md +6 -0
  37. package/registry/nocobase-auth-saml/auto-redirect-provider.tsx +13 -0
  38. package/registry/nocobase-auth-saml/demo.tsx +20 -0
  39. package/registry/nocobase-auth-saml/extension.tsx +51 -0
  40. package/registry/nocobase-auth-saml/index.ts +1 -0
  41. package/registry/nocobase-auth-saml/saml-sign-in-button.tsx +43 -0
  42. package/registry/nocobase-auth-saml/use-saml-sign-in.ts +51 -0
  43. package/registry/nocobase-auth-sms/README.md +7 -0
  44. package/registry/nocobase-auth-sms/demo.tsx +25 -0
  45. package/registry/nocobase-auth-sms/extension.tsx +46 -0
  46. package/registry/nocobase-auth-sms/index.ts +1 -0
  47. package/registry/nocobase-auth-sms/sms-sign-in-form.tsx +107 -0
  48. package/registry/nocobase-auth-sms/use-sms-sign-in.ts +88 -0
  49. package/registry/nocobase-auth-wecom/README.md +6 -0
  50. package/registry/nocobase-auth-wecom/auto-login-provider.tsx +21 -0
  51. package/registry/nocobase-auth-wecom/demo.tsx +23 -0
  52. package/registry/nocobase-auth-wecom/extension.tsx +49 -0
  53. package/registry/nocobase-auth-wecom/index.ts +1 -0
  54. package/registry/nocobase-auth-wecom/use-wecom-sign-in.ts +62 -0
  55. package/registry/nocobase-auth-wecom/wecom-sign-in-button.tsx +70 -0
  56. package/registry/nocobase-i18n/demo/prompt-generator.tsx +30 -54
  57. package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +18 -21
  58. package/registry.config.json +112 -0
  59. package/scripts/build-html.mjs +106 -0
  60. package/src/App.tsx +34 -8
  61. package/src/app/extension.ts +4 -0
  62. package/src/app/extensions.tsx +24 -1
  63. package/src/components/auth/authenticator-adapters.ts +20 -0
  64. package/src/components/auth/auto-redirect-provider.tsx +84 -0
  65. package/src/components/auth/basic-sign-in-form.tsx +101 -0
  66. package/src/components/auth/default-sign-in-page.tsx +20 -0
  67. package/src/components/auth/demo/auth-demo-page.tsx +236 -0
  68. package/src/components/auth/demo/auth-demo-prompt-generator.tsx +93 -0
  69. package/src/components/auth/demo/auth-demo-route.tsx +11 -0
  70. package/src/components/auth/demo/auth-method-demo.tsx +48 -0
  71. package/src/components/auth/demo/index.ts +4 -0
  72. package/src/components/auth/dynamic-sign-in.tsx +191 -0
  73. package/src/components/auth/forgot-password-form.tsx +13 -5
  74. package/src/components/auth/index.ts +13 -0
  75. package/src/components/auth/sign-in-form.tsx +1 -111
  76. package/src/components/auth/sign-up-form.tsx +14 -37
  77. package/src/components/auth/types.ts +38 -0
  78. package/src/components/auth/use-authenticator-sign-in.ts +14 -0
  79. package/src/components/auth/use-public-authenticators.ts +26 -0
  80. package/src/components/data-table/data-table-filter.tsx +107 -153
  81. package/src/components/demo/prompt-output.tsx +71 -0
  82. package/src/components/ui/alert-dialog.tsx +2 -0
  83. package/src/components/ui/attachment.tsx +207 -0
  84. package/src/components/ui/bubble.tsx +128 -0
  85. package/src/components/ui/button-group.tsx +87 -0
  86. package/src/components/ui/button.tsx +6 -6
  87. package/src/components/ui/calendar.tsx +0 -2
  88. package/src/components/ui/card.tsx +1 -1
  89. package/src/components/ui/carousel.tsx +2 -0
  90. package/src/components/ui/combobox.tsx +297 -0
  91. package/src/components/ui/command.tsx +43 -41
  92. package/src/components/ui/dialog.tsx +0 -2
  93. package/src/components/ui/direction.tsx +4 -0
  94. package/src/components/ui/drawer.tsx +2 -0
  95. package/src/components/ui/dropdown-menu.tsx +0 -2
  96. package/src/components/ui/empty.tsx +104 -0
  97. package/src/components/ui/field.tsx +238 -0
  98. package/src/components/ui/hover-card.tsx +2 -0
  99. package/src/components/ui/input-group.tsx +0 -2
  100. package/src/components/ui/input-otp.tsx +0 -2
  101. package/src/components/ui/input.tsx +1 -1
  102. package/src/components/ui/item.tsx +201 -0
  103. package/src/components/ui/kbd.tsx +26 -0
  104. package/src/components/ui/label.tsx +2 -0
  105. package/src/components/ui/marker.tsx +71 -0
  106. package/src/components/ui/message-scroller.tsx +131 -0
  107. package/src/components/ui/message.tsx +92 -0
  108. package/src/components/ui/native-select.tsx +61 -0
  109. package/src/components/ui/popover.tsx +0 -2
  110. package/src/components/ui/progress.tsx +2 -0
  111. package/src/components/ui/radio-group.tsx +0 -2
  112. package/src/components/ui/resizable.tsx +2 -0
  113. package/src/components/ui/scroll-area.tsx +0 -2
  114. package/src/components/ui/select.tsx +6 -4
  115. package/src/components/ui/separator.tsx +0 -2
  116. package/src/components/ui/sheet.tsx +21 -22
  117. package/src/components/ui/sidebar.tsx +1 -3
  118. package/src/components/ui/sonner.tsx +2 -0
  119. package/src/components/ui/spinner.tsx +10 -0
  120. package/src/components/ui/switch.tsx +0 -2
  121. package/src/components/ui/table.tsx +4 -2
  122. package/src/components/ui/tabs.tsx +0 -2
  123. package/src/components/ui/textarea.tsx +1 -1
  124. package/src/components/ui/toggle-group.tsx +2 -0
  125. package/src/components/ui/toggle.tsx +2 -0
  126. package/src/components/ui/tooltip.tsx +0 -2
  127. package/src/index.tsx +3 -0
  128. package/src/lib/nocobase/client.ts +32 -2
  129. package/src/lib/utils.ts +2 -1
  130. package/src/locales/en-US.ts +4 -2
  131. package/src/locales/zh-CN.ts +4 -2
  132. package/src/pages/login/index.tsx +2 -2
  133. package/src/pages/users/list.tsx +144 -16
  134. package/src/providers/auth-callback.ts +21 -0
  135. package/src/providers/auth.ts +108 -22
  136. package/src/providers/constants.ts +4 -1
  137. package/src/providers/runtime-config.ts +19 -0
  138. package/src/vite-env.d.ts +5 -0
  139. package/vite.config.ts +55 -2
@@ -1,24 +1,24 @@
1
- import * as React from "react";
2
- import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
1
+ import * as React from "react"
2
+ import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
3
3
 
4
- import { cn } from "@/lib/utils";
5
- import { Button } from "@/components/ui/button";
6
- import { XIcon } from "lucide-react";
4
+ import { cn } from "@/lib/utils"
5
+ import { Button } from "@/components/ui/button"
6
+ import { XIcon } from "lucide-react"
7
7
 
8
8
  function Sheet({ ...props }: SheetPrimitive.Root.Props) {
9
- return <SheetPrimitive.Root data-slot="sheet" {...props} />;
9
+ return <SheetPrimitive.Root data-slot="sheet" {...props} />
10
10
  }
11
11
 
12
12
  function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
13
- return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
13
+ return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
14
14
  }
15
15
 
16
16
  function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
17
- return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
17
+ return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
18
18
  }
19
19
 
20
20
  function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
21
- return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
21
+ return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
22
22
  }
23
23
 
24
24
  function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
@@ -31,7 +31,7 @@ function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
31
31
  )}
32
32
  {...props}
33
33
  />
34
- );
34
+ )
35
35
  }
36
36
 
37
37
  function SheetContent({
@@ -39,12 +39,10 @@ function SheetContent({
39
39
  children,
40
40
  side = "right",
41
41
  showCloseButton = true,
42
- closeLabel = "Close",
43
42
  ...props
44
43
  }: SheetPrimitive.Popup.Props & {
45
- side?: "top" | "right" | "bottom" | "left";
46
- showCloseButton?: boolean;
47
- closeLabel?: string;
44
+ side?: "top" | "right" | "bottom" | "left"
45
+ showCloseButton?: boolean
48
46
  }) {
49
47
  return (
50
48
  <SheetPortal>
@@ -70,13 +68,14 @@ function SheetContent({
70
68
  />
71
69
  }
72
70
  >
73
- <XIcon />
74
- <span className="sr-only">{closeLabel}</span>
71
+ <XIcon
72
+ />
73
+ <span className="sr-only">Close</span>
75
74
  </SheetPrimitive.Close>
76
75
  )}
77
76
  </SheetPrimitive.Popup>
78
77
  </SheetPortal>
79
- );
78
+ )
80
79
  }
81
80
 
82
81
  function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -86,7 +85,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
86
85
  className={cn("flex flex-col gap-0.5 p-4", className)}
87
86
  {...props}
88
87
  />
89
- );
88
+ )
90
89
  }
91
90
 
92
91
  function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -96,7 +95,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
96
95
  className={cn("mt-auto flex flex-col gap-2 p-4", className)}
97
96
  {...props}
98
97
  />
99
- );
98
+ )
100
99
  }
101
100
 
102
101
  function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
@@ -109,7 +108,7 @@ function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
109
108
  )}
110
109
  {...props}
111
110
  />
112
- );
111
+ )
113
112
  }
114
113
 
115
114
  function SheetDescription({
@@ -122,7 +121,7 @@ function SheetDescription({
122
121
  className={cn("text-sm text-muted-foreground", className)}
123
122
  {...props}
124
123
  />
125
- );
124
+ )
126
125
  }
127
126
 
128
127
  export {
@@ -134,4 +133,4 @@ export {
134
133
  SheetFooter,
135
134
  SheetTitle,
136
135
  SheetDescription,
137
- };
136
+ }
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import * as React from "react"
4
2
  import { mergeProps } from "@base-ui/react/merge-props"
5
3
  import { useRender } from "@base-ui/react/use-render"
@@ -29,7 +27,7 @@ const SIDEBAR_COOKIE_NAME = "sidebar_state"
29
27
  const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
30
28
  const SIDEBAR_WIDTH = "16rem"
31
29
  const SIDEBAR_WIDTH_MOBILE = "18rem"
32
- const SIDEBAR_WIDTH_ICON = "4.75rem"
30
+ const SIDEBAR_WIDTH_ICON = "3rem"
33
31
  const SIDEBAR_KEYBOARD_SHORTCUT = "b"
34
32
 
35
33
  type SidebarContextProps = {
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  import { useTheme } from "next-themes"
2
4
  import { Toaster as Sonner, type ToasterProps } from "sonner"
3
5
  import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
@@ -0,0 +1,10 @@
1
+ import { cn } from "@/lib/utils"
2
+ import { Loader2Icon } from "lucide-react"
3
+
4
+ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
5
+ return (
6
+ <Loader2Icon data-slot="spinner" role="status" aria-label="Loading" className={cn("size-4 animate-spin", className)} {...props} />
7
+ )
8
+ }
9
+
10
+ export { Spinner }
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
4
2
 
5
3
  import { cn } from "@/lib/utils"
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  import * as React from "react"
2
4
 
3
5
  import { cn } from "@/lib/utils"
@@ -68,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
68
70
  <th
69
71
  data-slot="table-head"
70
72
  className={cn(
71
- "h-11 px-4 text-left align-middle font-semibold whitespace-nowrap text-muted-foreground [&:has([role=checkbox])]:pr-0",
73
+ "h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
72
74
  className
73
75
  )}
74
76
  {...props}
@@ -81,7 +83,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
81
83
  <td
82
84
  data-slot="table-cell"
83
85
  className={cn(
84
- "px-4 py-3 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
86
+ "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
85
87
  className
86
88
  )}
87
89
  {...props}
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
4
2
  import { cva, type VariantProps } from "class-variance-authority"
5
3
 
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
7
7
  <textarea
8
8
  data-slot="textarea"
9
9
  className={cn(
10
- "flex field-sizing-content min-h-24 w-full rounded-lg border border-input bg-transparent px-3 py-2.5 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
10
+ "flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
11
11
  className
12
12
  )}
13
13
  {...props}
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  import * as React from "react"
2
4
  import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
3
5
  import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group"
@@ -1,3 +1,5 @@
1
+ "use client"
2
+
1
3
  import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
2
4
  import { cva, type VariantProps } from "class-variance-authority"
3
5
 
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
4
2
 
5
3
  import { cn } from "@/lib/utils"
package/src/index.tsx CHANGED
@@ -3,6 +3,9 @@ import { createRoot } from "react-dom/client";
3
3
 
4
4
  import "./locales";
5
5
  import App from "./App";
6
+ import { captureAuthenticationCallback } from "./providers/auth-callback";
7
+
8
+ captureAuthenticationCallback();
6
9
 
7
10
  const container = document.getElementById("root") as HTMLElement;
8
11
  const root = createRoot(container);
@@ -2,6 +2,7 @@ import {
2
2
  API_ORIGIN,
3
3
  API_URL,
4
4
  NOCOBASE_AUTHENTICATOR,
5
+ NOCOBASE_AUTHENTICATOR_KEY,
5
6
  NOCOBASE_LOCALE_KEY,
6
7
  NOCOBASE_ROLE_KEY,
7
8
  NOCOBASE_TOKEN_KEY,
@@ -22,6 +23,7 @@ export type NocoBaseRequestOptions = {
22
23
  body?: unknown;
23
24
  signal?: AbortSignal;
24
25
  token?: string;
26
+ authenticator?: string | null;
25
27
  role?: string;
26
28
  includeRole?: boolean;
27
29
  includeAuthenticator?: boolean;
@@ -93,6 +95,25 @@ export class NocoBaseClient {
93
95
  );
94
96
  }
95
97
 
98
+ getStoredAuthenticator() {
99
+ return typeof localStorage === "undefined"
100
+ ? undefined
101
+ : localStorage.getItem(NOCOBASE_AUTHENTICATOR_KEY) ?? undefined;
102
+ }
103
+
104
+ getAuthenticator() {
105
+ return this.getStoredAuthenticator() ?? NOCOBASE_AUTHENTICATOR;
106
+ }
107
+
108
+ setAuthenticator(authenticator?: string | null) {
109
+ if (typeof localStorage === "undefined") return;
110
+ if (authenticator) {
111
+ localStorage.setItem(NOCOBASE_AUTHENTICATOR_KEY, authenticator);
112
+ } else {
113
+ localStorage.removeItem(NOCOBASE_AUTHENTICATOR_KEY);
114
+ }
115
+ }
116
+
96
117
  setToken(token?: string | null) {
97
118
  if (typeof localStorage === "undefined") return;
98
119
  if (token) localStorage.setItem(NOCOBASE_TOKEN_KEY, token);
@@ -153,6 +174,7 @@ export class NocoBaseClient {
153
174
 
154
175
  getHeaders({
155
176
  token = this.getToken(),
177
+ authenticator,
156
178
  role = this.getRole(),
157
179
  includeRole = true,
158
180
  includeAuthenticator = false,
@@ -163,6 +185,7 @@ export class NocoBaseClient {
163
185
  }: Pick<
164
186
  NocoBaseRequestOptions,
165
187
  | "token"
188
+ | "authenticator"
166
189
  | "role"
167
190
  | "includeRole"
168
191
  | "includeAuthenticator"
@@ -171,6 +194,13 @@ export class NocoBaseClient {
171
194
  | "accept"
172
195
  | "body"
173
196
  > = {}) {
197
+ const resolvedAuthenticator =
198
+ authenticator === null
199
+ ? undefined
200
+ : authenticator ??
201
+ (includeAuthenticator
202
+ ? this.getAuthenticator()
203
+ : this.getStoredAuthenticator());
174
204
  const locale = this.getLocale();
175
205
  const formData =
176
206
  typeof FormData !== "undefined" && body instanceof FormData;
@@ -182,8 +212,8 @@ export class NocoBaseClient {
182
212
  ...(accept === "stream"
183
213
  ? { "Cache-Control": "no-cache", Pragma: "no-cache" }
184
214
  : {}),
185
- ...(includeAuthenticator
186
- ? { "X-Authenticator": NOCOBASE_AUTHENTICATOR }
215
+ ...(resolvedAuthenticator
216
+ ? { "X-Authenticator": resolvedAuthenticator }
187
217
  : {}),
188
218
  ...(includeRole && role ? { "X-Role": role } : {}),
189
219
  ...(withAclMeta ? { "X-With-ACL-Meta": "true" } : {}),
package/src/lib/utils.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { clsx, type ClassValue } from "clsx"
2
2
  import { twMerge } from "tailwind-merge"
3
+ import { getPortalBase } from "@/providers/runtime-config"
3
4
 
4
5
  export function cn(...inputs: ClassValue[]) {
5
6
  return twMerge(clsx(inputs))
@@ -8,7 +9,7 @@ export function cn(...inputs: ClassValue[]) {
8
9
  export function assetUrl(path: string) {
9
10
  if (/^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(path)) return path
10
11
 
11
- const base = import.meta.env.BASE_URL.replace(/\/+$/, "")
12
+ const base = getPortalBase().replace(/\/+$/, "")
12
13
  const assetPath = `/${path.replace(/^\/+/, "")}`
13
14
 
14
15
  if (base && (assetPath === base || assetPath.startsWith(`${base}/`))) {
@@ -23,8 +23,8 @@ export const starter = {
23
23
  "table.filter.operator.gt": "Greater than",
24
24
  "table.filter.operator.lte": "Less than or equal",
25
25
  "table.filter.operator.gte": "Greater than or equal",
26
- "table.filter.operator.in": "Includes in an array",
27
- "table.filter.operator.nin": "Not includes in an array",
26
+ "table.filter.operator.in": "Includes",
27
+ "table.filter.operator.nin": "Excludes",
28
28
  "table.filter.operator.ina": "Includes in an array (case sensitive)",
29
29
  "table.filter.operator.nina": "Not includes in an array (case sensitive)",
30
30
  "table.filter.operator.contains": "Contains",
@@ -101,6 +101,8 @@ export const app = {
101
101
  "users.fields.createdAt": "Created at",
102
102
  "users.fields.updatedAt": "Updated at",
103
103
  "users.fields.actions": "Actions",
104
+ "users.filters.roles.placeholder": "Select roles...",
105
+ "users.filters.roles.noResults": "No roles found.",
104
106
  "users.actions.view": "View user",
105
107
  "users.actions.edit": "Edit user",
106
108
  "users.actions.delete": "Delete user",
@@ -23,8 +23,8 @@ export const starter = {
23
23
  "table.filter.operator.gt": "大于",
24
24
  "table.filter.operator.lte": "小于或等于",
25
25
  "table.filter.operator.gte": "大于或等于",
26
- "table.filter.operator.in": "包含于数组",
27
- "table.filter.operator.nin": "不包含于数组",
26
+ "table.filter.operator.in": "包含",
27
+ "table.filter.operator.nin": "排除",
28
28
  "table.filter.operator.ina": "包含于数组(区分大小写)",
29
29
  "table.filter.operator.nina": "不包含于数组(区分大小写)",
30
30
  "table.filter.operator.contains": "包含",
@@ -97,6 +97,8 @@ export const app = {
97
97
  "users.fields.createdAt": "创建时间",
98
98
  "users.fields.updatedAt": "更新时间",
99
99
  "users.fields.actions": "操作",
100
+ "users.filters.roles.placeholder": "选择角色…",
101
+ "users.filters.roles.noResults": "未找到角色。",
100
102
  "users.actions.view": "查看用户",
101
103
  "users.actions.edit": "编辑用户",
102
104
  "users.actions.delete": "删除用户",
@@ -1,5 +1,5 @@
1
- import { SignInForm } from "@/components/auth/sign-in-form";
1
+ import { DefaultSignInPage } from "@/components/auth/default-sign-in-page";
2
2
 
3
3
  export const Login = () => {
4
- return <SignInForm />;
4
+ return <DefaultSignInPage />;
5
5
  };
@@ -1,11 +1,16 @@
1
- import { useGetLocale, useTranslate } from "@refinedev/core";
1
+ import { useGetLocale, useList, useTranslate } from "@refinedev/core";
2
2
  import { useTable } from "@refinedev/react-table";
3
- import { createColumnHelper } from "@tanstack/react-table";
3
+ import { createColumnHelper, type Column } from "@tanstack/react-table";
4
4
  import { Eye, Pencil, Trash2 } from "lucide-react";
5
- import { useCallback, useMemo } from "react";
5
+ import { useCallback, useMemo, type ReactNode } from "react";
6
6
  import { useNavigate } from "react-router";
7
7
 
8
8
  import { DataTable } from "@/components/data-table/data-table";
9
+ import {
10
+ DataTableFilterCombobox,
11
+ DataTableFilterDropdownText,
12
+ } from "@/components/data-table/data-table-filter";
13
+ import { DataTableSorter } from "@/components/data-table/data-table-sorter";
9
14
  import { DeleteButton } from "@/components/resources/buttons/delete";
10
15
  import { EditButton } from "@/components/resources/buttons/edit";
11
16
  import { ShowButton } from "@/components/resources/buttons/show";
@@ -15,11 +20,31 @@ import type { Role } from "@/lib/nocobase/acl";
15
20
  import { RoleBadges } from "./role-badges";
16
21
  import { resolveRoleLabel } from "./role-utils";
17
22
  import { getRolePath } from "./routes";
18
- import type { UserRecord } from "./types";
23
+ import type { RoleRecord, UserRecord } from "./types";
19
24
 
20
25
  const isRootUser = (record: UserRecord) =>
21
26
  record.roles?.some((role) => role.name === "root") ?? false;
22
27
 
28
+ function UserColumnHeader<TValue>({
29
+ children,
30
+ column,
31
+ label,
32
+ sortable = true,
33
+ }: {
34
+ children?: ReactNode;
35
+ column: Column<UserRecord, TValue>;
36
+ label: string;
37
+ sortable?: boolean;
38
+ }) {
39
+ return (
40
+ <div className="flex items-center gap-1">
41
+ <span>{label}</span>
42
+ {sortable ? <DataTableSorter column={column} /> : null}
43
+ {children}
44
+ </div>
45
+ );
46
+ }
47
+
23
48
  export const UserList = () => {
24
49
  const translate = useTranslate();
25
50
  const getLocale = useGetLocale();
@@ -29,6 +54,23 @@ export const UserList = () => {
29
54
  (role: Role) => navigate(getRolePath(role.name)),
30
55
  [navigate]
31
56
  );
57
+ const { result: rolesResult } = useList<RoleRecord>({
58
+ resource: "roles",
59
+ pagination: { mode: "server", currentPage: 1, pageSize: 200 },
60
+ errorNotification: false,
61
+ queryOptions: { retry: false },
62
+ });
63
+ const roleOptions = useMemo(
64
+ () =>
65
+ rolesResult.data
66
+ .filter((role) => role.name)
67
+ .map((role) => ({
68
+ value: role.name,
69
+ label: resolveRoleLabel(role),
70
+ }))
71
+ .sort((left, right) => left.label.localeCompare(right.label, locale)),
72
+ [locale, rolesResult.data]
73
+ );
32
74
 
33
75
  const columns = useMemo(() => {
34
76
  const columnHelper = createColumnHelper<UserRecord>();
@@ -36,32 +78,113 @@ export const UserList = () => {
36
78
  return [
37
79
  columnHelper.accessor("nickname", {
38
80
  id: "nickname",
39
- header: translate("users.fields.nickname", { ns: "app" }, "Nickname"),
81
+ header: ({ column, table }) => (
82
+ <UserColumnHeader
83
+ column={column}
84
+ label={translate(
85
+ "users.fields.nickname",
86
+ { ns: "app" },
87
+ "Nickname"
88
+ )}
89
+ >
90
+ <DataTableFilterDropdownText
91
+ column={column}
92
+ table={table}
93
+ defaultOperator="contains"
94
+ operators={["contains", "eq", "startswith", "endswith"]}
95
+ />
96
+ </UserColumnHeader>
97
+ ),
40
98
  enableSorting: true,
41
99
  cell: ({ row, getValue }) =>
42
100
  getValue() || row.original.username || row.original.email || "-",
43
101
  }),
44
102
  columnHelper.accessor("username", {
45
103
  id: "username",
46
- header: translate("users.fields.username", { ns: "app" }, "Username"),
104
+ header: ({ column, table }) => (
105
+ <UserColumnHeader
106
+ column={column}
107
+ label={translate(
108
+ "users.fields.username",
109
+ { ns: "app" },
110
+ "Username"
111
+ )}
112
+ >
113
+ <DataTableFilterDropdownText
114
+ column={column}
115
+ table={table}
116
+ defaultOperator="contains"
117
+ operators={["contains", "eq", "startswith", "endswith"]}
118
+ />
119
+ </UserColumnHeader>
120
+ ),
47
121
  enableSorting: true,
48
122
  cell: ({ getValue }) => getValue() || "-",
49
123
  }),
50
124
  columnHelper.accessor("email", {
51
125
  id: "email",
52
- header: translate("users.fields.email", { ns: "app" }, "Email"),
126
+ header: ({ column, table }) => (
127
+ <UserColumnHeader
128
+ column={column}
129
+ label={translate("users.fields.email", { ns: "app" }, "Email")}
130
+ >
131
+ <DataTableFilterDropdownText
132
+ column={column}
133
+ table={table}
134
+ defaultOperator="contains"
135
+ operators={["contains", "eq", "startswith", "endswith"]}
136
+ />
137
+ </UserColumnHeader>
138
+ ),
53
139
  enableSorting: true,
54
140
  cell: ({ getValue }) => getValue() || "-",
55
141
  }),
56
142
  columnHelper.accessor("phone", {
57
143
  id: "phone",
58
- header: translate("users.fields.phone", { ns: "app" }, "Phone"),
144
+ header: ({ column, table }) => (
145
+ <UserColumnHeader
146
+ column={column}
147
+ label={translate("users.fields.phone", { ns: "app" }, "Phone")}
148
+ >
149
+ <DataTableFilterDropdownText
150
+ column={column}
151
+ table={table}
152
+ defaultOperator="contains"
153
+ operators={["contains", "eq", "startswith", "endswith"]}
154
+ />
155
+ </UserColumnHeader>
156
+ ),
59
157
  enableSorting: true,
60
158
  cell: ({ getValue }) => getValue() || "-",
61
159
  }),
62
- columnHelper.accessor("roles", {
63
- id: "roles",
64
- header: translate("users.fields.roles", { ns: "app" }, "Roles"),
160
+ columnHelper.accessor((record) => record.roles, {
161
+ id: "roles.name",
162
+ header: ({ column, table }) => (
163
+ <UserColumnHeader
164
+ column={column}
165
+ label={translate("users.fields.roles", { ns: "app" }, "Roles")}
166
+ sortable={false}
167
+ >
168
+ <DataTableFilterCombobox
169
+ column={column}
170
+ table={table}
171
+ options={roleOptions}
172
+ defaultOperator="in"
173
+ operators={["in", "nin"]}
174
+ placeholder={translate(
175
+ "users.filters.roles.placeholder",
176
+ { ns: "app" },
177
+ "Select roles..."
178
+ )}
179
+ noResultsText={translate(
180
+ "users.filters.roles.noResults",
181
+ { ns: "app" },
182
+ "No roles found."
183
+ )}
184
+ multiple
185
+ />
186
+ </UserColumnHeader>
187
+ ),
65
188
  enableSorting: false,
66
189
  cell: ({ getValue }) => {
67
190
  const roles = getValue() ?? [];
@@ -70,10 +193,15 @@ export const UserList = () => {
70
193
  }),
71
194
  columnHelper.accessor("createdAt", {
72
195
  id: "createdAt",
73
- header: translate(
74
- "users.fields.createdAt",
75
- { ns: "app" },
76
- "Created at"
196
+ header: ({ column }) => (
197
+ <UserColumnHeader
198
+ column={column}
199
+ label={translate(
200
+ "users.fields.createdAt",
201
+ { ns: "app" },
202
+ "Created at"
203
+ )}
204
+ />
77
205
  ),
78
206
  enableSorting: true,
79
207
  cell: ({ getValue }) => {
@@ -153,7 +281,7 @@ export const UserList = () => {
153
281
  size: 144,
154
282
  }),
155
283
  ];
156
- }, [locale, showRole, translate]);
284
+ }, [locale, roleOptions, showRole, translate]);
157
285
 
158
286
  const table = useTable<UserRecord>({
159
287
  columns,
@@ -0,0 +1,21 @@
1
+ import { clearAcl } from "@/lib/nocobase/acl";
2
+ import { nocobaseClient } from "@/lib/nocobase/client";
3
+
4
+ export function captureAuthenticationCallback() {
5
+ if (typeof window === "undefined") return false;
6
+
7
+ const url = new URL(window.location.href);
8
+ const token = url.searchParams.get("token");
9
+ const authenticator = url.searchParams.get("authenticator");
10
+
11
+ if (!token) return false;
12
+
13
+ nocobaseClient.setToken(token);
14
+ nocobaseClient.setAuthenticator(authenticator);
15
+ clearAcl();
16
+
17
+ url.searchParams.delete("token");
18
+ url.searchParams.delete("authenticator");
19
+ window.history.replaceState(window.history.state, "", url);
20
+ return true;
21
+ }