@allbluecn/admin-app 0.3.1 → 0.3.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 (199) hide show
  1. package/package.json +19 -17
  2. package/src/components/admin/admin-user-menu.tsx +4 -4
  3. package/src/components/admin/developer-log.tsx +16 -32
  4. package/src/components/ai-memory/memory-facts-panel.tsx +30 -70
  5. package/src/components/ai-prompts/preview-panel.tsx +1 -1
  6. package/src/components/ai-prompts/prompt-editor.tsx +8 -9
  7. package/src/components/ai-prompts/prompt-table.tsx +6 -6
  8. package/src/components/ai-prompts/section-editor.tsx +3 -3
  9. package/src/components/ai-prompts/variable-panel.tsx +2 -2
  10. package/src/components/ai-prompts/version-history.tsx +21 -41
  11. package/src/components/ai-provider/model-list-editor.tsx +2 -3
  12. package/src/components/ai-provider/provider-form.tsx +7 -7
  13. package/src/components/ai-provider/provider-table.tsx +17 -40
  14. package/src/components/auth/admin-login-form.tsx +7 -7
  15. package/src/components/auth/icon-input.tsx +2 -2
  16. package/src/components/auth/password-input.tsx +2 -2
  17. package/src/components/component/component-form-dialog.tsx +10 -18
  18. package/src/components/component/component-import-dialog.tsx +6 -6
  19. package/src/components/component/component-preview.tsx +311 -0
  20. package/src/components/component/component-table.tsx +3 -3
  21. package/src/components/dependency/changelog-dialog.tsx +2 -2
  22. package/src/components/dependency/columns.tsx +3 -3
  23. package/src/components/dependency/dependency-table.tsx +7 -7
  24. package/src/components/dependency/edit-dialog.tsx +8 -8
  25. package/src/components/invitations/invitations-page.tsx +26 -49
  26. package/src/components/knowledge-public/bookmark-form-dialog.tsx +6 -6
  27. package/src/components/knowledge-public/compliance-form-dialog.tsx +6 -6
  28. package/src/components/knowledge-public/legal-form-dialog.tsx +7 -7
  29. package/src/components/layout/sidebar-02/app-sidebar.tsx +1 -2
  30. package/src/components/layout/sidebar-02/index.tsx +1 -1
  31. package/src/components/layout/sidebar-02/nav-main.tsx +2 -3
  32. package/src/components/layout/sidebar-02/nav-notifications.tsx +3 -3
  33. package/src/components/market/columns.tsx +4 -4
  34. package/src/components/market/market-app-form.tsx +6 -7
  35. package/src/components/market/market-app-list.tsx +14 -29
  36. package/src/components/monitoring/error-list.tsx +2 -2
  37. package/src/components/monitoring/metrics-stats.tsx +1 -1
  38. package/src/components/monitoring/request-log-table.tsx +2 -2
  39. package/src/components/oauth-providers/provider-page.tsx +20 -38
  40. package/src/components/permissions/assign-dialog.tsx +135 -0
  41. package/src/components/permissions/create-role-dialog.tsx +135 -0
  42. package/src/components/permissions/types.ts +49 -0
  43. package/src/components/user/columns.tsx +6 -6
  44. package/src/components/user/settings-activity-log.tsx +7 -7
  45. package/src/components/user/settings-profile/avatar-uploader.tsx +118 -0
  46. package/src/components/user/settings-profile/social-links.tsx +63 -0
  47. package/src/components/user/settings-profile.tsx +26 -125
  48. package/src/components/user/user-create-dialog.tsx +6 -6
  49. package/src/components/user/user-detail/error-boundary.tsx +19 -0
  50. package/src/components/user/user-detail/user-detail-dialogs.tsx +203 -0
  51. package/src/components/user/user-filters.tsx +4 -4
  52. package/src/lib/auth/page-gate.ts +28 -0
  53. package/src/lib/auth.config.ts +25 -9
  54. package/src/lib/i18n/detect.ts +1 -1
  55. package/src/lib/loader-serial.ts +14 -0
  56. package/src/paraglide/README.md +1 -1
  57. package/src/providers/index.tsx +1 -1
  58. package/src/routeTree.gen.ts +67 -78
  59. package/src/routes/__root.tsx +4 -4
  60. package/src/routes/_nav/audit/route.lazy.tsx +1 -2
  61. package/src/routes/_nav/compliance/route.lazy.tsx +5 -3
  62. package/src/routes/_nav/components/route.lazy.tsx +14 -34
  63. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +18 -38
  64. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +18 -38
  65. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +6 -6
  66. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +3 -3
  67. package/src/routes/_nav/licenses/new/route.lazy.tsx +5 -5
  68. package/src/routes/_nav/licenses/route.lazy.tsx +1 -1
  69. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +2 -2
  70. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +10 -10
  71. package/src/routes/_nav/monitoring/errors/route.tsx +7 -0
  72. package/src/routes/_nav/monitoring/health/route.lazy.tsx +13 -15
  73. package/src/routes/_nav/monitoring/health/route.tsx +14 -0
  74. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +24 -9
  75. package/src/routes/_nav/monitoring/logs/route.tsx +13 -0
  76. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +12 -14
  77. package/src/routes/_nav/monitoring/metrics/route.tsx +7 -0
  78. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +14 -13
  79. package/src/routes/_nav/monitoring/overview/route.tsx +17 -0
  80. package/src/routes/_nav/monitoring/route.lazy.tsx +6 -6
  81. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +2 -2
  82. package/src/routes/_nav/permissions/route.lazy.tsx +21 -311
  83. package/src/routes/_nav/permissions/route.tsx +1 -1
  84. package/src/routes/_nav/route.tsx +5 -13
  85. package/src/routes/_nav/sso/route.lazy.tsx +2 -3
  86. package/src/routes/_nav/users/$userId/route.lazy.tsx +91 -156
  87. package/src/routes/_nav/users/-components/users-table.tsx +1 -1
  88. package/src/routes/_nav/users/route.lazy.tsx +1 -1
  89. package/src/routes/login/route.lazy.tsx +1 -1
  90. package/src/routes/setup/route.lazy.tsx +2 -2
  91. package/src/server/__tests__/admin-billing.test.ts +191 -16
  92. package/src/server/__tests__/compliance.test.ts +44 -7
  93. package/src/server/__tests__/permissions.test.ts +10 -5
  94. package/src/server/__tests__/prompt-defaults.test.ts +3 -3
  95. package/src/server/admin-ai-alerts.ts +2 -1
  96. package/src/server/admin-ai-usage.ts +2 -1
  97. package/src/server/ai-memory/store.ts +6 -2
  98. package/src/server/{serverFns/ai-prompts → ai-prompts}/prompt-defaults.ts +5 -3
  99. package/src/server/{serverFns/dependency → dependency}/helpers.ts +4 -3
  100. package/src/server/knowledge-public/github-sync.ts +2 -1
  101. package/src/server/license-issue.ts +2 -1
  102. package/src/server/middlewares/auth-rate-limit.ts +117 -0
  103. package/src/server/observability/health-check.ts +4 -1
  104. package/src/server/observability/metrics.ts +17 -15
  105. package/src/server/observability/request-middleware.ts +2 -1
  106. package/src/server/observability/timing.ts +14 -16
  107. package/src/server/serverFns/admin-billing.ts +107 -12
  108. package/src/server/serverFns/ai-memory/list-users.ts +2 -1
  109. package/src/server/serverFns/ai-prompts/activate.ts +2 -1
  110. package/src/server/serverFns/ai-prompts/get.ts +3 -2
  111. package/src/server/serverFns/ai-prompts/list.ts +3 -2
  112. package/src/server/serverFns/ai-prompts/rollback.ts +3 -1
  113. package/src/server/serverFns/ai-prompts/save.ts +5 -3
  114. package/src/server/serverFns/ai-provider/create.ts +4 -1
  115. package/src/server/serverFns/ai-provider/delete.ts +4 -1
  116. package/src/server/serverFns/ai-provider/get.ts +4 -1
  117. package/src/server/serverFns/ai-provider/list.ts +28 -1
  118. package/src/server/serverFns/ai-provider/seed-from-seed.ts +2 -1
  119. package/src/server/serverFns/ai-provider/update.ts +4 -1
  120. package/src/server/serverFns/anyhub/market.ts +8 -1
  121. package/src/server/serverFns/audit.ts +14 -1
  122. package/src/server/serverFns/auth/setup.ts +3 -1
  123. package/src/server/serverFns/compliance.ts +32 -3
  124. package/src/server/serverFns/components/import.ts +69 -14
  125. package/src/server/serverFns/components/list.ts +4 -1
  126. package/src/server/serverFns/components/mutations.ts +9 -1
  127. package/src/server/serverFns/dependency/list.ts +28 -5
  128. package/src/server/serverFns/dependency/sync.ts +7 -2
  129. package/src/server/serverFns/dependency/update.ts +4 -1
  130. package/src/server/serverFns/developer-log-auto-generate.ts +63 -32
  131. package/src/server/serverFns/developer-logs.ts +5 -1
  132. package/src/server/serverFns/invitations.ts +7 -1
  133. package/src/server/serverFns/knowledge-public/bookmarks/create.ts +2 -1
  134. package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +2 -1
  135. package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +2 -1
  136. package/src/server/serverFns/knowledge-public/bookmarks/list.ts +2 -1
  137. package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +2 -1
  138. package/src/server/serverFns/knowledge-public/bookmarks/update.ts +2 -1
  139. package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
  140. package/src/server/serverFns/knowledge-public/compliance/delete.ts +2 -1
  141. package/src/server/serverFns/knowledge-public/compliance/detail.ts +2 -1
  142. package/src/server/serverFns/knowledge-public/compliance/list.ts +2 -1
  143. package/src/server/serverFns/knowledge-public/compliance/update.ts +2 -1
  144. package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
  145. package/src/server/serverFns/knowledge-public/legal/delete.ts +2 -1
  146. package/src/server/serverFns/knowledge-public/legal/detail.ts +2 -1
  147. package/src/server/serverFns/knowledge-public/legal/list.ts +2 -1
  148. package/src/server/serverFns/knowledge-public/legal/update.ts +2 -1
  149. package/src/server/serverFns/licenses.ts +8 -2
  150. package/src/server/serverFns/monitoring/get-errors.ts +2 -1
  151. package/src/server/serverFns/monitoring/get-health.ts +2 -1
  152. package/src/server/serverFns/monitoring/get-metrics.ts +2 -1
  153. package/src/server/serverFns/monitoring/get-request-logs.ts +2 -1
  154. package/src/server/serverFns/monitoring/report-error.ts +2 -1
  155. package/src/server/serverFns/monitoring/reset.ts +2 -1
  156. package/src/server/serverFns/oauth-providers.ts +4 -1
  157. package/src/server/serverFns/permissions.ts +18 -11
  158. package/src/server/serverFns/sentry/config.ts +11 -8
  159. package/src/server/serverFns/sso-config.ts +17 -2
  160. package/src/server/serverFns/user/create.ts +6 -1
  161. package/src/server/serverFns/user/delete.ts +6 -1
  162. package/src/server/serverFns/user/detail.ts +5 -2
  163. package/src/server/serverFns/user/list.ts +19 -4
  164. package/src/server/serverFns/user/reset-password.ts +10 -2
  165. package/src/server/serverFns/user/update-role.ts +10 -2
  166. package/src/server/serverFns/user/update-status.ts +10 -2
  167. package/src/styles/globals.css +4 -0
  168. package/src/components/data-table/data-table-pagination.tsx +0 -112
  169. package/src/components/data-table/data-table.tsx +0 -218
  170. package/src/components/data-table/table-detail-layout.tsx +0 -98
  171. package/src/components/ui/alert-dialog.tsx +0 -187
  172. package/src/components/ui/alert.tsx +0 -75
  173. package/src/components/ui/avatar.tsx +0 -106
  174. package/src/components/ui/badge.tsx +0 -51
  175. package/src/components/ui/button.tsx +0 -57
  176. package/src/components/ui/card.tsx +0 -102
  177. package/src/components/ui/checkbox.tsx +0 -28
  178. package/src/components/ui/collapsible.tsx +0 -19
  179. package/src/components/ui/dialog.tsx +0 -155
  180. package/src/components/ui/dropdown-menu.tsx +0 -267
  181. package/src/components/ui/field.tsx +0 -236
  182. package/src/components/ui/input-group.tsx +0 -158
  183. package/src/components/ui/input.tsx +0 -19
  184. package/src/components/ui/label.tsx +0 -17
  185. package/src/components/ui/pagination.tsx +0 -130
  186. package/src/components/ui/select.tsx +0 -234
  187. package/src/components/ui/separator.tsx +0 -22
  188. package/src/components/ui/sheet.tsx +0 -135
  189. package/src/components/ui/sidebar.tsx +0 -721
  190. package/src/components/ui/skeleton.tsx +0 -13
  191. package/src/components/ui/switch.tsx +0 -29
  192. package/src/components/ui/table.tsx +0 -115
  193. package/src/components/ui/textarea.tsx +0 -17
  194. package/src/components/ui/toast.tsx +0 -282
  195. package/src/components/ui/tooltip.tsx +0 -63
  196. package/src/hooks/use-mobile.ts +0 -19
  197. package/src/lib/avatar.ts +0 -41
  198. /package/src/routes/{api.auth.$.ts → api/auth/$.ts} +0 -0
  199. /package/src/routes/{api.sentry-test.ts → api/sentry-test.ts} +0 -0
@@ -0,0 +1,49 @@
1
+ export type ResourcePermission = Record<string, 'admin' | 'write' | 'read' | 'none'>
2
+
3
+ export type RoleItem = {
4
+ id: string
5
+ name: string
6
+ permissions: ResourcePermission
7
+ isSystem: boolean
8
+ _count: { assignments: number }
9
+ }
10
+
11
+ export type AssignmentItem = {
12
+ id: string
13
+ roleId: string
14
+ userId: string
15
+ user: {
16
+ id: string
17
+ username: string
18
+ email: string
19
+ }
20
+ createdAt: string | Date
21
+ }
22
+
23
+ export type SearchUser = {
24
+ id: string
25
+ username: string
26
+ email: string
27
+ }
28
+
29
+ export const ALL_RESOURCES: { key: string; labelZh: string; labelEn: string }[] = [
30
+ { key: 'knowledge', labelZh: '知识库', labelEn: 'Knowledge' },
31
+ { key: 'story', labelZh: '故事', labelEn: 'Story' },
32
+ { key: 'user', labelZh: '用户', labelEn: 'User' },
33
+ { key: 'setting', labelZh: '设置', labelEn: 'Setting' },
34
+ { key: 'billing', labelZh: '计费', labelEn: 'Billing' },
35
+ ]
36
+
37
+ export const ALL_ACTIONS: { key: string; labelZh: string; labelEn: string }[] = [
38
+ { key: 'read', labelZh: '读', labelEn: 'Read' },
39
+ { key: 'write', labelZh: '写', labelEn: 'Write' },
40
+ { key: 'delete', labelZh: '删', labelEn: 'Delete' },
41
+ { key: 'publish', labelZh: '发布', labelEn: 'Publish' },
42
+ ]
43
+
44
+ export const LEVELS: { value: 'none' | 'read' | 'write' | 'admin'; labelZh: string; labelEn: string }[] = [
45
+ { value: 'none', labelZh: '无', labelEn: 'None' },
46
+ { value: 'read', labelZh: '读', labelEn: 'Read' },
47
+ { value: 'write', labelZh: '写', labelEn: 'Write' },
48
+ { value: 'admin', labelZh: '管理', labelEn: 'Admin' },
49
+ ]
@@ -16,12 +16,12 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
 
19
- import { Badge } from "@/components/ui/badge";
20
- import { Button } from "@/components/ui/button";
19
+ import { Badge } from "@allbluecn/ui/badge";
20
+ import { Button } from "@allbluecn/ui/button";
21
21
  import { EyeIcon } from "lucide-react";
22
- import { UserAvatar } from "@allbluecn/web-app/user-avatar";
22
+ import { UserAvatar } from "@allbluecn/ui/components/user-avatar";
23
23
  import type { AvatarConfig } from "@allbluecn/kernel";
24
- import type { AdminColumnDef } from "@/components/data-table/data-table";
24
+ import type { AdminColumnDef } from "@allbluecn/ui/data-table";
25
25
 
26
26
  export type UserRow = {
27
27
  id: string;
@@ -36,7 +36,7 @@ export type UserRow = {
36
36
  updatedAt: Date;
37
37
  lastLoginAt: Date | null;
38
38
  _count: {
39
- prds: number;
39
+ designs: number;
40
40
  anyhubMarketApps: number;
41
41
  reviews: number;
42
42
  };
@@ -112,7 +112,7 @@ export function getUserColumns(onView: (user: UserRow) => void): AdminColumnDef<
112
112
  {
113
113
  id: "prdCount",
114
114
  header: "PRD",
115
- cell: ({ row }: any) => <span className="text-muted-foreground">{row.original._count.prds}</span>,
115
+ cell: ({ row }: any) => <span className="text-muted-foreground">{row.original._count.designs}</span>,
116
116
  },
117
117
  {
118
118
  id: "pluginCount",
@@ -26,28 +26,28 @@ import {
26
26
  import { useState } from "react";
27
27
  import { m } from '@/paraglide/messages.js'
28
28
  import { cn } from "@/lib/utils";
29
- import { Badge } from "@/components/ui/badge";
30
- import { Button } from "@/components/ui/button";
29
+ import { Badge } from "@allbluecn/ui/badge";
30
+ import { Button } from "@allbluecn/ui/button";
31
31
  import {
32
32
  Card,
33
33
  CardContent,
34
34
  CardDescription,
35
35
  CardHeader,
36
36
  CardTitle,
37
- } from "@/components/ui/card";
38
- import { FieldLabel } from "@/components/ui/field";
37
+ } from "@allbluecn/ui/card";
38
+ import { FieldLabel } from "@allbluecn/ui/field";
39
39
  import {
40
40
  InputGroup,
41
41
  InputGroupInput,
42
- } from "@/components/ui/input-group";
42
+ } from "@allbluecn/ui/input-group";
43
43
  import {
44
44
  Select,
45
45
  SelectContent,
46
46
  SelectItem,
47
47
  SelectTrigger,
48
48
  SelectValue,
49
- } from "@/components/ui/select";
50
- import { Separator } from "@/components/ui/separator";
49
+ } from "@allbluecn/ui/select";
50
+ import { Separator } from "@allbluecn/ui/separator";
51
51
 
52
52
  export interface ActivityLogEntry {
53
53
  id: string;
@@ -0,0 +1,118 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+ import { Camera, Loader2, X } from "lucide-react";
4
+ import { cn } from "@/lib/utils";
5
+ import { FieldLabel } from "@allbluecn/ui/field";
6
+ import { Button } from "@allbluecn/ui/button";
7
+
8
+ /**
9
+ * 头像上传区(纯展示):预览/拖拽/进度态由父组件持有——errors 是父级
10
+ * 共享对象(各处理器整体替换),状态上收避免拆分改变清空语义。
11
+ * readOnly 且无预览时返回 null(与拆分前的条件渲染一致)。
12
+ */
13
+ export function AvatarUploader({
14
+ readOnly,
15
+ avatarPreview,
16
+ isUploadingAvatar,
17
+ error,
18
+ onAvatarClick,
19
+ onRemove,
20
+ onDragOver,
21
+ onDrop,
22
+ onFileInputChange,
23
+ fileInputRef,
24
+ }: {
25
+ readOnly: boolean;
26
+ avatarPreview: string | null;
27
+ isUploadingAvatar: boolean;
28
+ error: string | undefined;
29
+ onAvatarClick: () => void;
30
+ onRemove: () => void;
31
+ onDragOver: (e: React.DragEvent) => void;
32
+ onDrop: (e: React.DragEvent) => void;
33
+ onFileInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
34
+ fileInputRef: React.RefObject<HTMLInputElement | null>;
35
+ }) {
36
+ if (readOnly && !avatarPreview) return null;
37
+
38
+ return (
39
+ <div className="flex flex-col gap-4">
40
+ <FieldLabel>头像</FieldLabel>
41
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
42
+ <div
43
+ className={cn(
44
+ "relative flex size-24 shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-dashed transition-colors",
45
+ readOnly && "cursor-default border-muted bg-muted/30",
46
+ !readOnly && (isUploadingAvatar
47
+ ? "border-primary bg-primary/5"
48
+ : "border-muted bg-muted/30 hover:border-primary/50"),
49
+ !readOnly && "cursor-pointer"
50
+ )}
51
+ {...(readOnly
52
+ ? {}
53
+ : {
54
+ onClick: onAvatarClick,
55
+ onDragOver: onDragOver,
56
+ onDrop: onDrop,
57
+ })}
58
+ >
59
+ {avatarPreview ? (
60
+ <>
61
+ <img
62
+ alt="头像"
63
+ className="size-full object-cover"
64
+ src={avatarPreview}
65
+ />
66
+ {!readOnly && isUploadingAvatar && (
67
+ <div className="absolute inset-0 flex items-center justify-center bg-background/80">
68
+ <Loader2 className="size-6 animate-spin text-primary" />
69
+ </div>
70
+ )}
71
+ </>
72
+ ) : (
73
+ <Camera className="size-8 text-muted-foreground" />
74
+ )}
75
+ </div>
76
+ {!readOnly && (
77
+ <div className="flex flex-1 flex-col gap-2">
78
+ <div className="flex flex-col gap-2 sm:flex-row">
79
+ <Button
80
+ onClick={onAvatarClick}
81
+ type="button"
82
+ variant="outline"
83
+ >
84
+ <Camera className="size-4" />
85
+ {avatarPreview ? "更换头像" : "上传头像"}
86
+ </Button>
87
+ {avatarPreview && (
88
+ <Button
89
+ onClick={onRemove}
90
+ type="button"
91
+ variant="outline"
92
+ >
93
+ <X className="size-4" />
94
+ 移除
95
+ </Button>
96
+ )}
97
+ </div>
98
+ <p className="text-muted-foreground text-xs">
99
+ 拖拽图片到此,或点击选择。最大 5MB。
100
+ </p>
101
+ {error && (
102
+ <p className="text-destructive text-xs">{error}</p>
103
+ )}
104
+ </div>
105
+ )}
106
+ {!readOnly && (
107
+ <input
108
+ accept="image/*"
109
+ className="hidden"
110
+ onChange={onFileInputChange}
111
+ ref={fileInputRef}
112
+ type="file"
113
+ />
114
+ )}
115
+ </div>
116
+ </div>
117
+ );
118
+ }
@@ -0,0 +1,63 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+ import {
4
+ Field,
5
+ FieldContent,
6
+ FieldLabel,
7
+ } from "@allbluecn/ui/field";
8
+ import {
9
+ InputGroup,
10
+ InputGroupInput,
11
+ } from "@allbluecn/ui/input-group";
12
+ import {
13
+ defaultSocialPlatforms,
14
+ socialPlatformLabels,
15
+ } from "./constants";
16
+
17
+ /**
18
+ * 社交链接区(纯展示):平台清单来自 constants,值读写经父组件
19
+ * formData.socialLinks / updateSocialLink。readOnly 时仅显示已填平台。
20
+ */
21
+ export function SocialLinks({
22
+ readOnly,
23
+ getSocialLink,
24
+ onUpdate,
25
+ }: {
26
+ readOnly: boolean;
27
+ getSocialLink: (platform: string) => string;
28
+ onUpdate: (platform: string, url: string) => void;
29
+ }) {
30
+ const visiblePlatforms = defaultSocialPlatforms.filter(
31
+ (platform) => !readOnly || getSocialLink(platform.id).trim()
32
+ );
33
+ if (visiblePlatforms.length === 0) return null;
34
+
35
+ return (
36
+ <div className="flex flex-col gap-4">
37
+ <FieldLabel>社交链接</FieldLabel>
38
+ <div className="flex flex-col gap-3">
39
+ {visiblePlatforms.map((platform) => (
40
+ <Field key={platform.id}>
41
+ <FieldLabel htmlFor={`social-${platform.id}`}>
42
+ {socialPlatformLabels[platform.id] || platform.label}
43
+ </FieldLabel>
44
+ <FieldContent>
45
+ <InputGroup>
46
+ <InputGroupInput
47
+ id={`social-${platform.id}`}
48
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
49
+ onUpdate(platform.id, e.target.value)
50
+ }
51
+ placeholder={platform.placeholder}
52
+ readOnly={readOnly}
53
+ type="url"
54
+ value={getSocialLink(platform.id)}
55
+ />
56
+ </InputGroup>
57
+ </FieldContent>
58
+ </Field>
59
+ ))}
60
+ </div>
61
+ </div>
62
+ );
63
+ }
@@ -15,34 +15,32 @@
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
- import { Camera, Loader2, Save, X } from "lucide-react";
18
+ import { Loader2, Save } from "lucide-react";
19
19
  import { useCallback, useRef, useState } from "react";
20
20
  import { cn } from "@/lib/utils";
21
- import { Button } from "@/components/ui/button";
21
+ import { Button } from "@allbluecn/ui/button";
22
22
  import {
23
23
  Card,
24
24
  CardContent,
25
25
  CardDescription,
26
26
  CardHeader,
27
27
  CardTitle,
28
- } from "@/components/ui/card";
28
+ } from "@allbluecn/ui/card";
29
29
  import {
30
30
  Field,
31
31
  FieldContent,
32
32
  FieldDescription,
33
33
  FieldError,
34
34
  FieldLabel,
35
- } from "@/components/ui/field";
35
+ } from "@allbluecn/ui/field";
36
36
  import {
37
37
  InputGroup,
38
38
  InputGroupInput,
39
- } from "@/components/ui/input-group";
40
- import { Separator } from "@/components/ui/separator";
41
- import { Textarea } from "@/components/ui/textarea";
42
- import {
43
- defaultSocialPlatforms,
44
- socialPlatformLabels,
45
- } from "./settings-profile/constants";
39
+ } from "@allbluecn/ui/input-group";
40
+ import { Separator } from "@allbluecn/ui/separator";
41
+ import { Textarea } from "@allbluecn/ui/textarea";
42
+ import { AvatarUploader } from "./settings-profile/avatar-uploader";
43
+ import { SocialLinks } from "./settings-profile/social-links";
46
44
  import type {
47
45
  ProfileData,
48
46
  SettingsProfileProps,
@@ -334,84 +332,18 @@ export default function SettingsProfile({
334
332
 
335
333
  {/* Avatar Upload */}
336
334
  {!(readOnly && !avatarPreview) && (
337
- <div className="flex flex-col gap-4">
338
- <FieldLabel>头像</FieldLabel>
339
- <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
340
- <div
341
- className={cn(
342
- "relative flex size-24 shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-dashed transition-colors",
343
- readOnly && "cursor-default border-muted bg-muted/30",
344
- !readOnly && (isUploadingAvatar
345
- ? "border-primary bg-primary/5"
346
- : "border-muted bg-muted/30 hover:border-primary/50"),
347
- !readOnly && "cursor-pointer"
348
- )}
349
- {...(readOnly
350
- ? {}
351
- : {
352
- onClick: handleAvatarClick,
353
- onDragOver: handleDragOver,
354
- onDrop: handleDrop,
355
- })}
356
- >
357
- {avatarPreview ? (
358
- <>
359
- <img
360
- alt="头像"
361
- className="size-full object-cover"
362
- src={avatarPreview}
363
- />
364
- {!readOnly && isUploadingAvatar && (
365
- <div className="absolute inset-0 flex items-center justify-center bg-background/80">
366
- <Loader2 className="size-6 animate-spin text-primary" />
367
- </div>
368
- )}
369
- </>
370
- ) : (
371
- <Camera className="size-8 text-muted-foreground" />
372
- )}
373
- </div>
374
- {!readOnly && (
375
- <div className="flex flex-1 flex-col gap-2">
376
- <div className="flex flex-col gap-2 sm:flex-row">
377
- <Button
378
- onClick={handleAvatarClick}
379
- type="button"
380
- variant="outline"
381
- >
382
- <Camera className="size-4" />
383
- {avatarPreview ? "更换头像" : "上传头像"}
384
- </Button>
385
- {avatarPreview && (
386
- <Button
387
- onClick={handleAvatarRemove}
388
- type="button"
389
- variant="outline"
390
- >
391
- <X className="size-4" />
392
- 移除
393
- </Button>
394
- )}
395
- </div>
396
- <p className="text-muted-foreground text-xs">
397
- 拖拽图片到此,或点击选择。最大 5MB。
398
- </p>
399
- {errors.avatar && (
400
- <p className="text-destructive text-xs">{errors.avatar}</p>
401
- )}
402
- </div>
403
- )}
404
- {!readOnly && (
405
- <input
406
- accept="image/*"
407
- className="hidden"
408
- onChange={handleFileInputChange}
409
- ref={fileInputRef}
410
- type="file"
411
- />
412
- )}
413
- </div>
414
- </div>
335
+ <AvatarUploader
336
+ readOnly={readOnly}
337
+ avatarPreview={avatarPreview}
338
+ isUploadingAvatar={isUploadingAvatar}
339
+ error={errors.avatar}
340
+ onAvatarClick={handleAvatarClick}
341
+ onRemove={handleAvatarRemove}
342
+ onDragOver={handleDragOver}
343
+ onDrop={handleDrop}
344
+ onFileInputChange={handleFileInputChange}
345
+ fileInputRef={fileInputRef}
346
+ />
415
347
  )}
416
348
 
417
349
  <Separator />
@@ -621,42 +553,11 @@ export default function SettingsProfile({
621
553
  <Separator />
622
554
 
623
555
  {/* Social Links */}
624
- {defaultSocialPlatforms
625
- .filter(
626
- (platform) => !readOnly || getSocialLink(platform.id).trim()
627
- )
628
- .length > 0 && (
629
- <div className="flex flex-col gap-4">
630
- <FieldLabel>社交链接</FieldLabel>
631
- <div className="flex flex-col gap-3">
632
- {defaultSocialPlatforms
633
- .filter(
634
- (platform) => !readOnly || getSocialLink(platform.id).trim()
635
- )
636
- .map((platform) => (
637
- <Field key={platform.id}>
638
- <FieldLabel htmlFor={`social-${platform.id}`}>
639
- {socialPlatformLabels[platform.id] || platform.label}
640
- </FieldLabel>
641
- <FieldContent>
642
- <InputGroup>
643
- <InputGroupInput
644
- id={`social-${platform.id}`}
645
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
646
- updateSocialLink(platform.id, e.target.value)
647
- }
648
- placeholder={platform.placeholder}
649
- readOnly={readOnly}
650
- type="url"
651
- value={getSocialLink(platform.id)}
652
- />
653
- </InputGroup>
654
- </FieldContent>
655
- </Field>
656
- ))}
657
- </div>
658
- </div>
659
- )}
556
+ <SocialLinks
557
+ readOnly={readOnly}
558
+ getSocialLink={getSocialLink}
559
+ onUpdate={updateSocialLink}
560
+ />
660
561
  </div>
661
562
  </CardContent>
662
563
  </Card>
@@ -23,19 +23,19 @@ import {
23
23
  DialogHeader,
24
24
  DialogTitle,
25
25
  DialogFooter,
26
- } from "@/components/ui/dialog";
27
- import { Button } from "@/components/ui/button";
28
- import { Input } from "@/components/ui/input";
29
- import { Label } from "@/components/ui/label";
26
+ } from "@allbluecn/ui/dialog";
27
+ import { Button } from "@allbluecn/ui/button";
28
+ import { Input } from "@allbluecn/ui/input";
29
+ import { Label } from "@allbluecn/ui/label";
30
30
  import {
31
31
  Select,
32
32
  SelectContent,
33
33
  SelectItem,
34
34
  SelectTrigger,
35
35
  SelectValue,
36
- } from "@/components/ui/select";
36
+ } from "@allbluecn/ui/select";
37
37
  import { createUserFn } from "@/server/serverFns/user/create";
38
- import { toast } from '@/components/ui/toast';
38
+ import { toast } from '@allbluecn/ui/toast';
39
39
 
40
40
  interface UserCreateDialogProps {
41
41
  open: boolean;
@@ -0,0 +1,19 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+ import { m } from '@/paraglide/messages.js'
4
+ import { AlertCircle } from 'lucide-react'
5
+ import { Button } from '@allbluecn/ui/button'
6
+
7
+ export function AdminUserErrorBoundary({ error, reset }: { error: unknown; reset: () => void }) {
8
+ return <AdminUserErrorBoundaryInner error={error instanceof Error ? error : new Error(String(error))} reset={reset} />
9
+ }
10
+
11
+ function AdminUserErrorBoundaryInner({ error, reset }: { error: Error; reset: () => void }) {
12
+ return (
13
+ <div className="flex flex-col items-center justify-center h-64 gap-3">
14
+ <AlertCircle className="size-8 text-destructive" />
15
+ <p className="text-sm text-muted-foreground">{m['admin-users_detail_errorLoading']()}: {error.message}</p>
16
+ <Button variant="outline" size="sm" onClick={reset}>{m['admin-users_detail_retry']()}</Button>
17
+ </div>
18
+ )
19
+ }